Just Render the D2 Diagram, Please

Ready.

About

Just Render the D2 Diagram, Please is a minimal offline D2 diagram viewer and renderer.

Generated and human-reviewed by Al Sweigart. This app works offline and you own it forever. No ads. No registration. No subscriptions. No trackers. Just right-click and save the .html page.

Nothing is uploaded and all data stays on your computer.

METADATA: applyAppMetadata() updates metadata at runtime. Social crawlers often do not execute JavaScript, so a published fork should ALSO copy APP.name, APP.description, and APP.shareUrl into the static tags above. ============================================================================ */ const APP = Object.freeze({ name: "Just Render the D2 Diagram, Please", version: "0.1.0", description: "Just Render the D2 Diagram, Please is a minimal offline D2 diagram viewer and renderer.", shareUrl: "", defaultFilename: "d2-diagram", source: Object.freeze({ extension: "d2", // no leading period downloadLabel: "D2", // raw-source button label acceptedExtensions: Object.freeze([".d2", ".txt"]), acceptedMimeTypes: Object.freeze(["text/plain"]), defaultMimeType: "text/plain;charset=utf-8" }), render: Object.freeze({ debounceMs: 180, // Blank D2 is not rendered. Once a valid diagram exists, blank input keeps // that last-good SVG visible and exportable by framework design. renderBlankSource: false }), bitmap: Object.freeze({ basePixelDensity: 2, // 100% export scale = 2× bitmap density defaultExportScalePercent: 100, minExportScalePercent: 1, maxExportScalePercent: 800, maxDimension: 16384 }), output: Object.freeze({ previewBackground: "checkerboard", preserveTransparency: true, exportBackground: "transparent" }), development: Object.freeze({ showTemplateTestImage: false, runSelfTests: true }) }); /* ============================================================================ TRANSLATIONS The dropdown is derived from Object.keys(TRANSLATIONS). Deleting a language entry removes it from the UI automatically. Values missing in a translation fall back to English. The {extension} token in the localized Open label is replaced from APP.source.extension, avoiding hard-coded .txt assumptions. TO REDUCE FILE SIZE, DELETE ALL THE LANGUAGES YOU DON'T WANT SUPPORTED. ============================================================================ */ const TRANSLATIONS = { "en": { "title": "Generic Text Renderer Template", "open": "Open {extension}", "tidy": "Tidy", "examples": "Examples", "share": "Share", "about": "About", "language": "Language", "fit": "Fit", "rendered": "Rendered", "copied": "Copied to clipboard.", "close": "Close", "namePlaceholder": "Name", "zoomOut": "Zoom out", "zoomIn": "Zoom in", "zoomPercentage": "Zoom percentage", "exportScale": "Export Scale:", "rendering": "Rendering…", "empty": "Empty source.", "invalid": "Invalid source.", "noRenderer": "No renderer configured.", "exportFailed": "Export failed.", "openFailed": "Could not open file.", "sourceDownload": "Source download" }, "zh-CN": { "title": "Generic Text Renderer Template", "open": "打开 {extension}", "tidy": "整理", "examples": "演示", "share": "分享", "about": "关于", "language": "语言", "fit": "适应", "rendered": "已渲染", "copied": "已复制到剪贴板。", "close": "关闭", "namePlaceholder": "名称", "zoomOut": "缩小", "zoomIn": "放大", "zoomPercentage": "缩放百分比", "exportScale": "Export Scale:" }, "es": { "title": "Generic Text Renderer Template", "open": "Abrir {extension}", "tidy": "Formatear", "examples": "Demos", "share": "Compartir", "about": "Acerca de", "language": "Idioma", "fit": "Ajustar", "rendered": "Renderizado", "copied": "Copiado al portapapeles.", "close": "Cerrar", "namePlaceholder": "Nombre", "zoomOut": "Alejar", "zoomIn": "Acercar", "zoomPercentage": "Porcentaje de zoom", "exportScale": "Export Scale:" }, "ar": { "title": "Generic Text Renderer Template", "open": "فتح {extension}", "tidy": "تنسيق", "examples": "أمثلة", "share": "مشاركة", "about": "حول", "language": "اللغة", "fit": "ملاءمة", "rendered": "تم العرض", "copied": "تم النسخ إلى الحافظة.", "close": "إغلاق", "namePlaceholder": "الاسم", "zoomOut": "تصغير", "zoomIn": "تكبير", "zoomPercentage": "نسبة التكبير", "exportScale": "Export Scale:" }, "id": { "title": "Generic Text Renderer Template", "open": "Buka {extension}", "tidy": "Rapikan", "examples": "Demo", "share": "Bagikan", "about": "Tentang", "language": "Bahasa", "fit": "Sesuaikan", "rendered": "Selesai dirender", "copied": "Disalin ke papan klip.", "close": "Tutup", "namePlaceholder": "Nama", "zoomOut": "Perkecil", "zoomIn": "Perbesar", "zoomPercentage": "Persentase zoom", "exportScale": "Export Scale:" }, "pt": { "title": "Generic Text Renderer Template", "open": "Abrir {extension}", "tidy": "Formatar", "examples": "Demonstrações", "share": "Partilhar", "about": "Acerca de", "language": "Idioma", "fit": "Ajustar", "rendered": "Renderizado", "copied": "Copiado para a área de transferência.", "close": "Fechar", "namePlaceholder": "Nome", "zoomOut": "Reduzir zoom", "zoomIn": "Aumentar zoom", "zoomPercentage": "Percentagem de zoom", "exportScale": "Export Scale:" }, "fr": { "title": "Generic Text Renderer Template", "open": "Ouvrir un {extension}", "tidy": "Mettre en forme", "examples": "Démos", "share": "Partager", "about": "À propos", "language": "Langue", "fit": "Ajuster", "rendered": "Rendu terminé", "copied": "Copié dans le presse-papiers.", "close": "Fermer", "namePlaceholder": "Nom", "zoomOut": "Dézoomer", "zoomIn": "Zoomer", "zoomPercentage": "Pourcentage de zoom", "exportScale": "Export Scale:" }, "ja": { "title": "Generic Text Renderer Template", "open": "{extension}を開く", "tidy": "整形", "examples": "デモ", "share": "共有", "about": "このアプリについて", "language": "言語", "fit": "全体表示", "rendered": "レンダリング完了", "copied": "クリップボードにコピーしました。", "close": "閉じる", "namePlaceholder": "名前", "zoomOut": "縮小", "zoomIn": "拡大", "zoomPercentage": "ズーム率", "exportScale": "Export Scale:" }, "ru": { "title": "Generic Text Renderer Template", "open": "Открыть {extension}", "tidy": "Форматировать", "examples": "Демо", "share": "Поделиться", "about": "О программе", "language": "Язык", "fit": "Вписать", "rendered": "Отрисовано", "copied": "Скопировано в буфер обмена.", "close": "Закрыть", "namePlaceholder": "Имя", "zoomOut": "Уменьшить", "zoomIn": "Увеличить", "zoomPercentage": "Масштаб в процентах", "exportScale": "Export Scale:" }, "de": { "title": "Generic Text Renderer Template", "open": "{extension} öffnen", "tidy": "Formatieren", "examples": "Demos", "share": "Teilen", "about": "Info", "language": "Sprache", "fit": "Einpassen", "rendered": "Gerendert", "copied": "In die Zwischenablage kopiert.", "close": "Schließen", "namePlaceholder": "Name", "zoomOut": "Verkleinern", "zoomIn": "Vergrößern", "zoomPercentage": "Zoom in Prozent", "exportScale": "Export Scale:" }, "hi": { "title": "Generic Text Renderer Template", "open": "{extension} खोलें", "tidy": "फ़ॉर्मैट करें", "examples": "डेमो", "share": "साझा करें", "about": "परिचय", "language": "भाषा", "fit": "फ़िट करें", "rendered": "रेंडर किया गया", "copied": "क्लिपबोर्ड पर कॉपी किया गया।", "close": "बंद करें", "namePlaceholder": "नाम", "zoomOut": "ज़ूम आउट", "zoomIn": "ज़ूम इन", "zoomPercentage": "ज़ूम प्रतिशत", "exportScale": "Export Scale:" }, "bn": { "title": "Generic Text Renderer Template", "open": "{extension} খুলুন", "tidy": "ফরম্যাট করুন", "examples": "ডেমো", "share": "শেয়ার করুন", "about": "সম্পর্কে", "language": "ভাষা", "fit": "ফিট করুন", "rendered": "রেন্ডার হয়েছে", "copied": "ক্লিপবোর্ডে কপি করা হয়েছে।", "close": "বন্ধ করুন", "namePlaceholder": "নাম", "zoomOut": "জুম আউট", "zoomIn": "জুম ইন", "zoomPercentage": "জুম শতাংশ", "exportScale": "Export Scale:" }, "ur": { "title": "Generic Text Renderer Template", "open": "{extension} کھولیں", "tidy": "فارمیٹ کریں", "examples": "ڈیموز", "share": "شیئر کریں", "about": "تعارف", "language": "زبان", "fit": "فِٹ کریں", "rendered": "رینڈر ہو گیا", "copied": "کلپ بورڈ پر کاپی ہو گیا۔", "close": "بند کریں", "namePlaceholder": "نام", "zoomOut": "زوم آؤٹ", "zoomIn": "زوم اِن", "zoomPercentage": "زوم فیصد", "exportScale": "Export Scale:" }, "ko": { "title": "Generic Text Renderer Template", "open": "{extension} 열기", "tidy": "정리", "examples": "데모", "share": "공유", "about": "정보", "language": "언어", "fit": "맞춤", "rendered": "렌더링됨", "copied": "클립보드에 복사했습니다.", "close": "닫기", "namePlaceholder": "이름", "zoomOut": "축소", "zoomIn": "확대", "zoomPercentage": "확대/축소 비율", "exportScale": "Export Scale:" }, "vi": { "title": "Generic Text Renderer Template", "open": "Mở {extension}", "tidy": "Định dạng", "examples": "Bản mẫu", "share": "Chia sẻ", "about": "Giới thiệu", "language": "Ngôn ngữ", "fit": "Vừa khung", "rendered": "Đã kết xuất", "copied": "Đã sao chép vào bảng tạm.", "close": "Đóng", "namePlaceholder": "Tên", "zoomOut": "Thu nhỏ", "zoomIn": "Phóng to", "zoomPercentage": "Phần trăm thu phóng", "exportScale": "Export Scale:" }, "tr": { "title": "Generic Text Renderer Template", "open": "{extension} Aç", "tidy": "Biçimlendir", "examples": "Demolar", "share": "Paylaş", "about": "Hakkında", "language": "Dil", "fit": "Sığdır", "rendered": "Oluşturuldu", "copied": "Panoya kopyalandı.", "close": "Kapat", "namePlaceholder": "Ad", "zoomOut": "Uzaklaştır", "zoomIn": "Yakınlaştır", "zoomPercentage": "Yakınlaştırma yüzdesi", "exportScale": "Export Scale:" }, "it": { "title": "Generic Text Renderer Template", "open": "Apri {extension}", "tidy": "Formatta", "examples": "Demo", "share": "Condividi", "about": "Informazioni", "language": "Lingua", "fit": "Adatta", "rendered": "Renderizzato", "copied": "Copiato negli appunti.", "close": "Chiudi", "namePlaceholder": "Nome", "zoomOut": "Riduci zoom", "zoomIn": "Aumenta zoom", "zoomPercentage": "Percentuale di zoom", "exportScale": "Export Scale:" }, "nl": { "title": "Generic Text Renderer Template", "open": "{extension} openen", "tidy": "Formatteren", "examples": "Demo’s", "share": "Delen", "about": "Over", "language": "Taal", "fit": "Passend maken", "rendered": "Gerenderd", "copied": "Naar het klembord gekopieerd.", "close": "Sluiten", "namePlaceholder": "Naam", "zoomOut": "Uitzoomen", "zoomIn": "Inzoomen", "zoomPercentage": "Zoompercentage", "exportScale": "Export Scale:" }, "pl": { "title": "Generic Text Renderer Template", "open": "Otwórz {extension}", "tidy": "Formatuj", "examples": "Dema", "share": "Udostępnij", "about": "O aplikacji", "language": "Język", "fit": "Dopasuj", "rendered": "Wyrenderowano", "copied": "Skopiowano do schowka.", "close": "Zamknij", "namePlaceholder": "Nazwa", "zoomOut": "Pomniejsz", "zoomIn": "Powiększ", "zoomPercentage": "Procent powiększenia", "exportScale": "Export Scale:" }, "th": { "title": "Generic Text Renderer Template", "open": "เปิด {extension}", "tidy": "จัดรูปแบบ", "examples": "ตัวอย่าง", "share": "แชร์", "about": "เกี่ยวกับ", "language": "ภาษา", "fit": "พอดี", "rendered": "เรนเดอร์แล้ว", "copied": "คัดลอกไปยังคลิปบอร์ดแล้ว", "close": "ปิด", "namePlaceholder": "ชื่อ", "zoomOut": "ซูมออก", "zoomIn": "ซูมเข้า", "zoomPercentage": "เปอร์เซ็นต์การซูม", "exportScale": "Export Scale:" }, "fa": { "title": "Generic Text Renderer Template", "open": "باز کردن {extension}", "tidy": "قالب‌بندی", "examples": "نمونه‌ها", "share": "اشتراک‌گذاری", "about": "درباره", "language": "زبان", "fit": "جا دادن", "rendered": "رندر شد", "copied": "در کلیپ‌بورد کپی شد.", "close": "بستن", "namePlaceholder": "نام", "zoomOut": "کوچک‌نمایی", "zoomIn": "بزرگ‌نمایی", "zoomPercentage": "درصد بزرگ‌نمایی", "exportScale": "Export Scale:" }, "uk": { "title": "Generic Text Renderer Template", "open": "Відкрити {extension}", "tidy": "Форматувати", "examples": "Демо", "share": "Поділитися", "about": "Про програму", "language": "Мова", "fit": "Вписати", "rendered": "Відтворено", "copied": "Скопійовано в буфер обміну.", "close": "Закрити", "namePlaceholder": "Назва", "zoomOut": "Зменшити", "zoomIn": "Збільшити", "zoomPercentage": "Масштаб у відсотках", "exportScale": "Export Scale:" }, "cs": { "title": "Generic Text Renderer Template", "open": "Otevřít {extension}", "tidy": "Formátovat", "examples": "Ukázky", "share": "Sdílet", "about": "O aplikaci", "language": "Jazyk", "fit": "Přizpůsobit", "rendered": "Vykresleno", "copied": "Zkopírováno do schránky.", "close": "Zavřít", "namePlaceholder": "Název", "zoomOut": "Oddálit", "zoomIn": "Přiblížit", "zoomPercentage": "Procento přiblížení", "exportScale": "Export Scale:" }, "ms": { "title": "Generic Text Renderer Template", "open": "Buka {extension}", "tidy": "Format", "examples": "Demo", "share": "Kongsi", "about": "Perihal", "language": "Bahasa", "fit": "Muat", "rendered": "Selesai dirender", "copied": "Disalin ke papan klip.", "close": "Tutup", "namePlaceholder": "Nama", "zoomOut": "Zum keluar", "zoomIn": "Zum masuk", "zoomPercentage": "Peratus zum", "exportScale": "Export Scale:" }, "ro": { "title": "Generic Text Renderer Template", "open": "Deschide {extension}", "tidy": "Formatează", "examples": "Demonstrații", "share": "Distribuie", "about": "Despre", "language": "Limbă", "fit": "Potrivește", "rendered": "Redată", "copied": "Copiat în clipboard.", "close": "Închide", "namePlaceholder": "Nume", "zoomOut": "Micșorează", "zoomIn": "Mărește", "zoomPercentage": "Procent de zoom", "exportScale": "Export Scale:" }, "el": { "title": "Generic Text Renderer Template", "open": "Άνοιγμα {extension}", "tidy": "Μορφοποίηση", "examples": "Παραδείγματα", "share": "Κοινοποίηση", "about": "Σχετικά", "language": "Γλώσσα", "fit": "Προσαρμογή", "rendered": "Αποδόθηκε", "copied": "Αντιγράφηκε στο πρόχειρο.", "close": "Κλείσιμο", "namePlaceholder": "Όνομα", "zoomOut": "Σμίκρυνση", "zoomIn": "Μεγέθυνση", "zoomPercentage": "Ποσοστό ζουμ", "exportScale": "Export Scale:" }, "he": { "title": "Generic Text Renderer Template", "open": "פתיחת {extension}", "tidy": "עיצוב", "examples": "הדגמות", "share": "שיתוף", "about": "אודות", "language": "שפה", "fit": "התאמה", "rendered": "התרשים רונדר", "copied": "הועתק ללוח.", "close": "סגירה", "namePlaceholder": "שם", "zoomOut": "הקטנה", "zoomIn": "הגדלה", "zoomPercentage": "אחוז זום", "exportScale": "Export Scale:" }, "sv": { "title": "Generic Text Renderer Template", "open": "Öppna {extension}", "tidy": "Formatera", "examples": "Demon", "share": "Dela", "about": "Om", "language": "Språk", "fit": "Anpassa", "rendered": "Renderat", "copied": "Kopierat till urklipp.", "close": "Stäng", "namePlaceholder": "Namn", "zoomOut": "Zooma ut", "zoomIn": "Zooma in", "zoomPercentage": "Zoomprocent", "exportScale": "Export Scale:" }, "fil": { "title": "Generic Text Renderer Template", "open": "Buksan ang {extension}", "tidy": "I-format", "examples": "Mga demo", "share": "Ibahagi", "about": "Tungkol", "language": "Wika", "fit": "I-fit", "rendered": "Na-render", "copied": "Nakopya sa clipboard.", "close": "Isara", "namePlaceholder": "Pangalan", "zoomOut": "Mag-zoom out", "zoomIn": "Mag-zoom in", "zoomPercentage": "Porsiyento ng zoom", "exportScale": "Export Scale:" }, "ta": { "title": "Generic Text Renderer Template", "open": "{extension} திறக்கவும்", "tidy": "வடிவமைக்கவும்", "examples": "மாதிரிகள்", "share": "பகிர்", "about": "பற்றி", "language": "மொழி", "fit": "பொருத்து", "rendered": "ரெண்டர் செய்யப்பட்டது", "copied": "கிளிப்போர்டுக்கு நகலெடுக்கப்பட்டது.", "close": "மூடு", "namePlaceholder": "பெயர்", "zoomOut": "சிறிதாக்கு", "zoomIn": "பெரிதாக்கு", "zoomPercentage": "பெரிதாக்கல் சதவீதம்", "exportScale": "Export Scale:" }, "hu": { "title": "Generic Text Renderer Template", "open": "{extension} megnyitása", "tidy": "Formázás", "examples": "Demók", "share": "Megosztás", "about": "Névjegy", "language": "Nyelv", "fit": "Illesztés", "rendered": "Renderelve", "copied": "Vágólapra másolva.", "close": "Bezárás", "namePlaceholder": "Név", "zoomOut": "Kicsinyítés", "zoomIn": "Nagyítás", "zoomPercentage": "Nagyítás százalékban", "exportScale": "Export Scale:" }, "da": { "title": "Generic Text Renderer Template", "open": "Åbn {extension}", "tidy": "Formatér", "examples": "Demoer", "share": "Del", "about": "Om", "language": "Sprog", "fit": "Tilpas", "rendered": "Renderet", "copied": "Kopieret til udklipsholderen.", "close": "Luk", "namePlaceholder": "Navn", "zoomOut": "Zoom ud", "zoomIn": "Zoom ind", "zoomPercentage": "Zoomprocent", "exportScale": "Export Scale:" }, "fi": { "title": "Generic Text Renderer Template", "open": "Avaa {extension}", "tidy": "Muotoile", "examples": "Demot", "share": "Jaa", "about": "Tietoja", "language": "Kieli", "fit": "Sovita", "rendered": "Renderöity", "copied": "Kopioitu leikepöydälle.", "close": "Sulje", "namePlaceholder": "Nimi", "zoomOut": "Loitonna", "zoomIn": "Lähennä", "zoomPercentage": "Zoomausprosentti", "exportScale": "Export Scale:" }, "nb": { "title": "Generic Text Renderer Template", "open": "Åpne {extension}", "tidy": "Formater", "examples": "Demoer", "share": "Del", "about": "Om", "language": "Språk", "fit": "Tilpass", "rendered": "Gjengitt", "copied": "Kopiert til utklippstavlen.", "close": "Lukk", "namePlaceholder": "Navn", "zoomOut": "Zoom ut", "zoomIn": "Zoom inn", "zoomPercentage": "Zoomprosent", "exportScale": "Export Scale:" }, "sk": { "title": "Generic Text Renderer Template", "open": "Otvoriť {extension}", "tidy": "Formátovať", "examples": "Ukážky", "share": "Zdieľať", "about": "O aplikácii", "language": "Jazyk", "fit": "Prispôsobiť", "rendered": "Vykreslené", "copied": "Skopírované do schránky.", "close": "Zavrieť", "namePlaceholder": "Názov", "zoomOut": "Oddialiť", "zoomIn": "Priblížiť", "zoomPercentage": "Percento priblíženia", "exportScale": "Export Scale:" }, "bg": { "title": "Generic Text Renderer Template", "open": "Отвори {extension}", "tidy": "Форматирай", "examples": "Демонстрации", "share": "Сподели", "about": "Относно", "language": "Език", "fit": "Побери", "rendered": "Визуализирано", "copied": "Копирано в клипборда.", "close": "Затвори", "namePlaceholder": "Име", "zoomOut": "Намали", "zoomIn": "Увеличи", "zoomPercentage": "Процент мащабиране", "exportScale": "Export Scale:" }, "sr": { "title": "Generic Text Renderer Template", "open": "Отвори {extension}", "tidy": "Форматирај", "examples": "Демо", "share": "Подели", "about": "О програму", "language": "Језик", "fit": "Уклопи", "rendered": "Приказано", "copied": "Копирано у привремену меморију.", "close": "Затвори", "namePlaceholder": "Назив", "zoomOut": "Умањи", "zoomIn": "Увећај", "zoomPercentage": "Проценат зумирања", "exportScale": "Export Scale:" }, "hr": { "title": "Generic Text Renderer Template", "open": "Otvori {extension}", "tidy": "Formatiraj", "examples": "Demo primjeri", "share": "Podijeli", "about": "O aplikaciji", "language": "Jezik", "fit": "Prilagodi", "rendered": "Prikazano", "copied": "Kopirano u međuspremnik.", "close": "Zatvori", "namePlaceholder": "Naziv", "zoomOut": "Umanji", "zoomIn": "Uvećaj", "zoomPercentage": "Postotak zumiranja", "exportScale": "Export Scale:" }, "sl": { "title": "Generic Text Renderer Template", "open": "Odpri {extension}", "tidy": "Oblikuj", "examples": "Predstavitve", "share": "Deli", "about": "O aplikaciji", "language": "Jezik", "fit": "Prilagodi", "rendered": "Izrisano", "copied": "Kopirano v odložišče.", "close": "Zapri", "namePlaceholder": "Ime", "zoomOut": "Pomanjšaj", "zoomIn": "Povečaj", "zoomPercentage": "Odstotek povečave", "exportScale": "Export Scale:" }, "ca": { "title": "Generic Text Renderer Template", "open": "Obre {extension}", "tidy": "Formata", "examples": "Demos", "share": "Comparteix", "about": "Quant a", "language": "Idioma", "fit": "Ajusta", "rendered": "Renderitzat", "copied": "Copiat al porta-retalls.", "close": "Tanca", "namePlaceholder": "Nom", "zoomOut": "Allunya", "zoomIn": "Apropa", "zoomPercentage": "Percentatge de zoom", "exportScale": "Export Scale:" } }; /* D2-SPECIFIC TRANSLATIONS ------------------------ The base template supplies 40 language objects. This fork keeps all 40 and augments each with translated product, renderer-control, D2 status/error, About-description, and accessibility strings. A parity pass then fills any older generic template key that a language lacked from English. The renderer controls call the same t() function and are retranslated on language changes. */ const D2_TRANSLATIONS = { "en": { "title": "Just Render the D2 Diagram, Please", "aboutDescription": "Just Render the D2 Diagram, Please is a minimal offline D2 diagram viewer and renderer.", "layout": "Layout", "sketch": "Sketch", "ready": "Ready.", "rendering": "Rendering D2…", "empty": "Empty D2 source.", "invalid": "Invalid D2 source.", "d2Initializing": "Initializing D2…", "d2CompileError": "D2 could not compile the source.", "d2RenderError": "D2 could not render the diagram.", "d2ImportUnsupported": "D2 imports are unavailable in this single-file editor. Paste imported D2 into the editor instead.", "d2ExternalResource": "External or sibling images/icons are blocked. Use an embedded data:image URI instead.", "sourceEditor": "D2 source editor", "sourceText": "D2 source text", "renderedPreview": "Rendered D2 diagram preview", "previewFrameTitle": "Rendered D2 diagram", "layoutAria": "D2 layout engine", "sketchAria": "D2 sketch mode" }, "zh-CN": { "title": "只渲染 D2 图表就好", "aboutDescription": "只渲染 D2 图表就好是一款极简的离线 D2 图表查看与渲染工具。", "layout": "布局", "sketch": "手绘", "ready": "就绪。", "rendering": "正在渲染 D2…", "empty": "D2 源文本为空。", "invalid": "D2 源文本无效。", "d2Initializing": "正在初始化 D2…", "d2CompileError": "D2 无法编译源文本。", "d2RenderError": "D2 无法渲染图表。", "d2ImportUnsupported": "此单文件编辑器不支持 D2 导入。请将被导入的 D2 内容粘贴到编辑器中。", "d2ExternalResource": "已阻止外部或同级图片/图标。请改用内嵌的 data:image URI。", "sourceEditor": "D2 源文本编辑器", "sourceText": "D2 源文本", "renderedPreview": "已渲染的 D2 图表预览", "previewFrameTitle": "已渲染的 D2 图表", "layoutAria": "D2 布局引擎", "sketchAria": "D2 手绘模式" }, "es": { "title": "Solo renderiza el diagrama D2, por favor", "aboutDescription": "Solo renderiza el diagrama D2, por favor es un visor y renderizador D2 mínimo y sin conexión.", "layout": "Diseño", "sketch": "Boceto", "ready": "Listo.", "rendering": "Renderizando D2…", "empty": "Fuente D2 vacía.", "invalid": "Fuente D2 no válida.", "d2Initializing": "Inicializando D2…", "d2CompileError": "D2 no pudo compilar la fuente.", "d2RenderError": "D2 no pudo renderizar el diagrama.", "d2ImportUnsupported": "Las importaciones D2 no están disponibles en este editor de un solo archivo. Pega el D2 importado en el editor.", "d2ExternalResource": "Las imágenes o iconos externos o contiguos están bloqueados. Usa un URI data:image incrustado.", "sourceEditor": "Editor de fuente D2", "sourceText": "Texto fuente D2", "renderedPreview": "Vista previa del diagrama D2 renderizado", "previewFrameTitle": "Diagrama D2 renderizado", "layoutAria": "Motor de diseño D2", "sketchAria": "Modo boceto D2" }, "ar": { "title": "اعرض مخطط D2 فقط، من فضلك", "aboutDescription": "اعرض مخطط D2 فقط، من فضلك هو عارض ومصيّر مبسط لمخططات D2 يعمل دون اتصال.", "layout": "التخطيط", "sketch": "رسم يدوي", "ready": "جاهز.", "rendering": "جارٍ تصيير D2…", "empty": "مصدر D2 فارغ.", "invalid": "مصدر D2 غير صالح.", "d2Initializing": "جارٍ تهيئة D2…", "d2CompileError": "تعذر على D2 ترجمة المصدر.", "d2RenderError": "تعذر على D2 تصيير المخطط.", "d2ImportUnsupported": "استيرادات D2 غير متاحة في محرر الملف الواحد هذا. ألصق محتوى D2 المستورد في المحرر.", "d2ExternalResource": "تم حظر الصور أو الأيقونات الخارجية أو المجاورة. استخدم data:image مضمّنًا.", "sourceEditor": "محرر مصدر D2", "sourceText": "نص مصدر D2", "renderedPreview": "معاينة مخطط D2 المصيّر", "previewFrameTitle": "مخطط D2 المصيّر", "layoutAria": "محرك تخطيط D2", "sketchAria": "وضع الرسم اليدوي D2" }, "id": { "title": "Tolong Render Diagram D2 Saja", "aboutDescription": "Tolong Render Diagram D2 Saja adalah penampil dan perender diagram D2 offline yang minimal.", "layout": "Tata letak", "sketch": "Sketsa", "ready": "Siap.", "rendering": "Merender D2…", "empty": "Sumber D2 kosong.", "invalid": "Sumber D2 tidak valid.", "d2Initializing": "Menginisialisasi D2…", "d2CompileError": "D2 tidak dapat mengompilasi sumber.", "d2RenderError": "D2 tidak dapat merender diagram.", "d2ImportUnsupported": "Impor D2 tidak tersedia di editor satu file ini. Tempel D2 yang diimpor ke editor.", "d2ExternalResource": "Gambar atau ikon eksternal/sejajar diblokir. Gunakan URI data:image yang disematkan.", "sourceEditor": "Editor sumber D2", "sourceText": "Teks sumber D2", "renderedPreview": "Pratinjau diagram D2 yang dirender", "previewFrameTitle": "Diagram D2 yang dirender", "layoutAria": "Mesin tata letak D2", "sketchAria": "Mode sketsa D2" }, "pt": { "title": "Apenas renderize o diagrama D2, por favor", "aboutDescription": "Apenas renderize o diagrama D2, por favor é um visualizador e renderizador D2 mínimo e offline.", "layout": "Layout", "sketch": "Esboço", "ready": "Pronto.", "rendering": "A renderizar D2…", "empty": "Fonte D2 vazia.", "invalid": "Fonte D2 inválida.", "d2Initializing": "A inicializar D2…", "d2CompileError": "O D2 não conseguiu compilar a fonte.", "d2RenderError": "O D2 não conseguiu renderizar o diagrama.", "d2ImportUnsupported": "As importações D2 não estão disponíveis neste editor de ficheiro único. Cole o D2 importado no editor.", "d2ExternalResource": "Imagens ou ícones externos ou adjacentes estão bloqueados. Use um URI data:image incorporado.", "sourceEditor": "Editor de fonte D2", "sourceText": "Texto fonte D2", "renderedPreview": "Pré-visualização do diagrama D2 renderizado", "previewFrameTitle": "Diagrama D2 renderizado", "layoutAria": "Motor de layout D2", "sketchAria": "Modo de esboço D2" }, "fr": { "title": "Rendez simplement le diagramme D2, s’il vous plaît", "aboutDescription": "Rendez simplement le diagramme D2, s’il vous plaît est un visualiseur et moteur de rendu D2 minimal hors ligne.", "layout": "Disposition", "sketch": "Croquis", "ready": "Prêt.", "rendering": "Rendu D2…", "empty": "Source D2 vide.", "invalid": "Source D2 non valide.", "d2Initializing": "Initialisation de D2…", "d2CompileError": "D2 n’a pas pu compiler la source.", "d2RenderError": "D2 n’a pas pu rendre le diagramme.", "d2ImportUnsupported": "Les imports D2 ne sont pas disponibles dans cet éditeur à fichier unique. Collez le D2 importé dans l’éditeur.", "d2ExternalResource": "Les images ou icônes externes ou voisines sont bloquées. Utilisez une URI data:image intégrée.", "sourceEditor": "Éditeur de source D2", "sourceText": "Texte source D2", "renderedPreview": "Aperçu du diagramme D2 rendu", "previewFrameTitle": "Diagramme D2 rendu", "layoutAria": "Moteur de disposition D2", "sketchAria": "Mode croquis D2" }, "ja": { "title": "D2 ダイアグラムだけレンダリングしてください", "aboutDescription": "D2 ダイアグラムだけレンダリングしてくださいは、最小構成のオフライン D2 ダイアグラム表示・レンダリングツールです。", "layout": "レイアウト", "sketch": "スケッチ", "ready": "準備完了。", "rendering": "D2 をレンダリング中…", "empty": "D2 ソースが空です。", "invalid": "D2 ソースが無効です。", "d2Initializing": "D2 を初期化中…", "d2CompileError": "D2 はソースをコンパイルできませんでした。", "d2RenderError": "D2 はダイアグラムをレンダリングできませんでした。", "d2ImportUnsupported": "この単一ファイルエディターでは D2 の import は使えません。import 先の D2 をエディターへ貼り付けてください。", "d2ExternalResource": "外部または隣接ファイルの画像・アイコンはブロックされます。埋め込み data:image URI を使用してください。", "sourceEditor": "D2 ソースエディター", "sourceText": "D2 ソーステキスト", "renderedPreview": "レンダリング済み D2 ダイアグラムのプレビュー", "previewFrameTitle": "レンダリング済み D2 ダイアグラム", "layoutAria": "D2 レイアウトエンジン", "sketchAria": "D2 スケッチモード" }, "ru": { "title": "Просто отрисуй диаграмму D2, пожалуйста", "aboutDescription": "Просто отрисуй диаграмму D2, пожалуйста — минимальный автономный просмотрщик и рендерер диаграмм D2.", "layout": "Компоновка", "sketch": "Эскиз", "ready": "Готово.", "rendering": "Отрисовка D2…", "empty": "Пустой исходный текст D2.", "invalid": "Некорректный исходный текст D2.", "d2Initializing": "Инициализация D2…", "d2CompileError": "D2 не удалось скомпилировать исходный текст.", "d2RenderError": "D2 не удалось отрисовать диаграмму.", "d2ImportUnsupported": "Импорты D2 недоступны в этом однофайловом редакторе. Вставьте импортируемый D2 в редактор.", "d2ExternalResource": "Внешние или соседние изображения/значки заблокированы. Используйте встроенный URI data:image.", "sourceEditor": "Редактор исходного D2", "sourceText": "Исходный текст D2", "renderedPreview": "Предпросмотр отрисованной диаграммы D2", "previewFrameTitle": "Отрисованная диаграмма D2", "layoutAria": "Движок компоновки D2", "sketchAria": "Режим эскиза D2" }, "de": { "title": "Bitte nur das D2-Diagramm rendern", "aboutDescription": "Bitte nur das D2-Diagramm rendern ist ein minimaler Offline-Betrachter und Renderer für D2-Diagramme.", "layout": "Layout", "sketch": "Skizze", "ready": "Bereit.", "rendering": "D2 wird gerendert…", "empty": "Leere D2-Quelle.", "invalid": "Ungültige D2-Quelle.", "d2Initializing": "D2 wird initialisiert…", "d2CompileError": "D2 konnte die Quelle nicht kompilieren.", "d2RenderError": "D2 konnte das Diagramm nicht rendern.", "d2ImportUnsupported": "D2-Importe sind in diesem Ein-Datei-Editor nicht verfügbar. Füge den importierten D2-Inhalt in den Editor ein.", "d2ExternalResource": "Externe oder benachbarte Bilder/Symbole sind blockiert. Verwende eine eingebettete data:image-URI.", "sourceEditor": "D2-Quelltexteditor", "sourceText": "D2-Quelltext", "renderedPreview": "Vorschau des gerenderten D2-Diagramms", "previewFrameTitle": "Gerendertes D2-Diagramm", "layoutAria": "D2-Layout-Engine", "sketchAria": "D2-Skizzenmodus" }, "hi": { "title": "कृपया केवल D2 आरेख रेंडर करें", "aboutDescription": "कृपया केवल D2 आरेख रेंडर करें एक न्यूनतम ऑफ़लाइन D2 आरेख व्यूअर और रेंडरर है।", "layout": "लेआउट", "sketch": "स्केच", "ready": "तैयार।", "rendering": "D2 रेंडर हो रहा है…", "empty": "D2 स्रोत खाली है।", "invalid": "D2 स्रोत अमान्य है।", "d2Initializing": "D2 आरंभ हो रहा है…", "d2CompileError": "D2 स्रोत को कम्पाइल नहीं कर सका।", "d2RenderError": "D2 आरेख को रेंडर नहीं कर सका।", "d2ImportUnsupported": "इस एकल-फ़ाइल संपादक में D2 इम्पोर्ट उपलब्ध नहीं हैं। आयातित D2 को संपादक में पेस्ट करें।", "d2ExternalResource": "बाहरी या साथ वाली छवियाँ/आइकन अवरुद्ध हैं। एम्बेड किया हुआ data:image URI उपयोग करें।", "sourceEditor": "D2 स्रोत संपादक", "sourceText": "D2 स्रोत पाठ", "renderedPreview": "रेंडर किए गए D2 आरेख का पूर्वावलोकन", "previewFrameTitle": "रेंडर किया गया D2 आरेख", "layoutAria": "D2 लेआउट इंजन", "sketchAria": "D2 स्केच मोड" }, "bn": { "title": "অনুগ্রহ করে শুধু D2 ডায়াগ্রাম রেন্ডার করুন", "aboutDescription": "অনুগ্রহ করে শুধু D2 ডায়াগ্রাম রেন্ডার করুন একটি ন্যূনতম অফলাইন D2 ডায়াগ্রাম ভিউয়ার ও রেন্ডারার।", "layout": "লেআউট", "sketch": "স্কেচ", "ready": "প্রস্তুত।", "rendering": "D2 রেন্ডার হচ্ছে…", "empty": "D2 সোর্স খালি।", "invalid": "D2 সোর্স অবৈধ।", "d2Initializing": "D2 শুরু হচ্ছে…", "d2CompileError": "D2 সোর্স কম্পাইল করতে পারেনি।", "d2RenderError": "D2 ডায়াগ্রাম রেন্ডার করতে পারেনি।", "d2ImportUnsupported": "এই একক-ফাইল এডিটরে D2 ইমপোর্ট পাওয়া যায় না। ইমপোর্ট করা D2 এডিটরে পেস্ট করুন।", "d2ExternalResource": "বাহ্যিক বা পাশের ছবি/আইকন ব্লক করা হয়েছে। এমবেড করা data:image URI ব্যবহার করুন।", "sourceEditor": "D2 সোর্স এডিটর", "sourceText": "D2 সোর্স টেক্সট", "renderedPreview": "রেন্ডার করা D2 ডায়াগ্রামের প্রিভিউ", "previewFrameTitle": "রেন্ডার করা D2 ডায়াগ্রাম", "layoutAria": "D2 লেআউট ইঞ্জিন", "sketchAria": "D2 স্কেচ মোড" }, "ur": { "title": "براہِ کرم صرف D2 ڈایاگرام رینڈر کریں", "aboutDescription": "براہِ کرم صرف D2 ڈایاگرام رینڈر کریں ایک کم سے کم آف لائن D2 ڈایاگرام ویور اور رینڈرر ہے۔", "layout": "لے آؤٹ", "sketch": "اسکیچ", "ready": "تیار۔", "rendering": "D2 رینڈر ہو رہا ہے…", "empty": "D2 ماخذ خالی ہے۔", "invalid": "D2 ماخذ درست نہیں۔", "d2Initializing": "D2 شروع ہو رہا ہے…", "d2CompileError": "D2 ماخذ کو کمپائل نہیں کر سکا۔", "d2RenderError": "D2 ڈایاگرام رینڈر نہیں کر سکا۔", "d2ImportUnsupported": "اس ایک فائل ایڈیٹر میں D2 امپورٹس دستیاب نہیں۔ امپورٹ شدہ D2 کو ایڈیٹر میں چسپاں کریں۔", "d2ExternalResource": "بیرونی یا ساتھ والی تصاویر/آئیکنز مسدود ہیں۔ ایمبیڈڈ data:image URI استعمال کریں۔", "sourceEditor": "D2 ماخذ ایڈیٹر", "sourceText": "D2 ماخذ متن", "renderedPreview": "رینڈر شدہ D2 ڈایاگرام کا پیش منظر", "previewFrameTitle": "رینڈر شدہ D2 ڈایاگرام", "layoutAria": "D2 لے آؤٹ انجن", "sketchAria": "D2 اسکیچ موڈ" }, "ko": { "title": "D2 다이어그램만 렌더링해 주세요", "aboutDescription": "D2 다이어그램만 렌더링해 주세요는 최소한의 오프라인 D2 다이어그램 뷰어 및 렌더러입니다.", "layout": "레이아웃", "sketch": "스케치", "ready": "준비됨.", "rendering": "D2 렌더링 중…", "empty": "D2 소스가 비어 있습니다.", "invalid": "D2 소스가 올바르지 않습니다.", "d2Initializing": "D2 초기화 중…", "d2CompileError": "D2가 소스를 컴파일하지 못했습니다.", "d2RenderError": "D2가 다이어그램을 렌더링하지 못했습니다.", "d2ImportUnsupported": "이 단일 파일 편집기에서는 D2 가져오기를 사용할 수 없습니다. 가져올 D2를 편집기에 붙여 넣으세요.", "d2ExternalResource": "외부 또는 형제 파일 이미지/아이콘은 차단됩니다. 포함된 data:image URI를 사용하세요.", "sourceEditor": "D2 소스 편집기", "sourceText": "D2 소스 텍스트", "renderedPreview": "렌더링된 D2 다이어그램 미리보기", "previewFrameTitle": "렌더링된 D2 다이어그램", "layoutAria": "D2 레이아웃 엔진", "sketchAria": "D2 스케치 모드" }, "vi": { "title": "Chỉ kết xuất sơ đồ D2, làm ơn", "aboutDescription": "Chỉ kết xuất sơ đồ D2, làm ơn là trình xem và kết xuất sơ đồ D2 ngoại tuyến tối giản.", "layout": "Bố cục", "sketch": "Phác thảo", "ready": "Sẵn sàng.", "rendering": "Đang kết xuất D2…", "empty": "Nguồn D2 trống.", "invalid": "Nguồn D2 không hợp lệ.", "d2Initializing": "Đang khởi tạo D2…", "d2CompileError": "D2 không thể biên dịch nguồn.", "d2RenderError": "D2 không thể kết xuất sơ đồ.", "d2ImportUnsupported": "Trình soạn thảo một tệp này không hỗ trợ nhập D2. Hãy dán nội dung D2 được nhập vào trình soạn thảo.", "d2ExternalResource": "Hình ảnh/biểu tượng bên ngoài hoặc tệp kề bị chặn. Hãy dùng URI data:image được nhúng.", "sourceEditor": "Trình soạn thảo nguồn D2", "sourceText": "Văn bản nguồn D2", "renderedPreview": "Xem trước sơ đồ D2 đã kết xuất", "previewFrameTitle": "Sơ đồ D2 đã kết xuất", "layoutAria": "Bộ máy bố cục D2", "sketchAria": "Chế độ phác thảo D2" }, "tr": { "title": "Lütfen sadece D2 diyagramını oluştur", "aboutDescription": "Lütfen sadece D2 diyagramını oluştur, minimal ve çevrimdışı bir D2 diyagram görüntüleyici ve oluşturucudur.", "layout": "Yerleşim", "sketch": "Eskiz", "ready": "Hazır.", "rendering": "D2 oluşturuluyor…", "empty": "D2 kaynağı boş.", "invalid": "D2 kaynağı geçersiz.", "d2Initializing": "D2 başlatılıyor…", "d2CompileError": "D2 kaynağı derleyemedi.", "d2RenderError": "D2 diyagramı oluşturamadı.", "d2ImportUnsupported": "Bu tek dosyalı düzenleyicide D2 içe aktarmaları kullanılamaz. İçe aktarılan D2 içeriğini düzenleyiciye yapıştırın.", "d2ExternalResource": "Harici veya komşu resimler/simgeler engellenir. Gömülü bir data:image URI kullanın.", "sourceEditor": "D2 kaynak düzenleyicisi", "sourceText": "D2 kaynak metni", "renderedPreview": "Oluşturulan D2 diyagramı önizlemesi", "previewFrameTitle": "Oluşturulan D2 diyagramı", "layoutAria": "D2 yerleşim motoru", "sketchAria": "D2 eskiz modu" }, "it": { "title": "Renderizza solo il diagramma D2, per favore", "aboutDescription": "Renderizza solo il diagramma D2, per favore è un visualizzatore e renderer D2 offline minimale.", "layout": "Layout", "sketch": "Schizzo", "ready": "Pronto.", "rendering": "Rendering D2…", "empty": "Sorgente D2 vuota.", "invalid": "Sorgente D2 non valida.", "d2Initializing": "Inizializzazione di D2…", "d2CompileError": "D2 non ha potuto compilare il sorgente.", "d2RenderError": "D2 non ha potuto renderizzare il diagramma.", "d2ImportUnsupported": "Le importazioni D2 non sono disponibili in questo editor a file singolo. Incolla il D2 importato nell’editor.", "d2ExternalResource": "Immagini o icone esterne o adiacenti sono bloccate. Usa un URI data:image incorporato.", "sourceEditor": "Editor del sorgente D2", "sourceText": "Testo sorgente D2", "renderedPreview": "Anteprima del diagramma D2 renderizzato", "previewFrameTitle": "Diagramma D2 renderizzato", "layoutAria": "Motore di layout D2", "sketchAria": "Modalità schizzo D2" }, "nl": { "title": "Render alleen het D2-diagram, alstublieft", "aboutDescription": "Render alleen het D2-diagram, alstublieft is een minimale offline D2-diagramviewer en -renderer.", "layout": "Lay-out", "sketch": "Schets", "ready": "Gereed.", "rendering": "D2 wordt gerenderd…", "empty": "Lege D2-bron.", "invalid": "Ongeldige D2-bron.", "d2Initializing": "D2 wordt geïnitialiseerd…", "d2CompileError": "D2 kon de bron niet compileren.", "d2RenderError": "D2 kon het diagram niet renderen.", "d2ImportUnsupported": "D2-imports zijn niet beschikbaar in deze editor met één bestand. Plak de geïmporteerde D2 in de editor.", "d2ExternalResource": "Externe of naastliggende afbeeldingen/pictogrammen zijn geblokkeerd. Gebruik een ingebedde data:image-URI.", "sourceEditor": "D2-broneditor", "sourceText": "D2-brontekst", "renderedPreview": "Voorbeeld van gerenderd D2-diagram", "previewFrameTitle": "Gerenderd D2-diagram", "layoutAria": "D2-lay-outengine", "sketchAria": "D2-schetsmodus" }, "pl": { "title": "Proszę tylko wyrenderować diagram D2", "aboutDescription": "Proszę tylko wyrenderować diagram D2 to minimalna, działająca offline przeglądarka i renderer diagramów D2.", "layout": "Układ", "sketch": "Szkic", "ready": "Gotowe.", "rendering": "Renderowanie D2…", "empty": "Puste źródło D2.", "invalid": "Nieprawidłowe źródło D2.", "d2Initializing": "Inicjalizacja D2…", "d2CompileError": "D2 nie mógł skompilować źródła.", "d2RenderError": "D2 nie mógł wyrenderować diagramu.", "d2ImportUnsupported": "Importy D2 nie są dostępne w tym jednoplikowym edytorze. Wklej importowany D2 do edytora.", "d2ExternalResource": "Zewnętrzne lub sąsiednie obrazy/ikony są blokowane. Użyj osadzonego URI data:image.", "sourceEditor": "Edytor źródła D2", "sourceText": "Tekst źródłowy D2", "renderedPreview": "Podgląd wyrenderowanego diagramu D2", "previewFrameTitle": "Wyrenderowany diagram D2", "layoutAria": "Silnik układu D2", "sketchAria": "Tryb szkicu D2" }, "th": { "title": "กรุณาเรนเดอร์เฉพาะไดอะแกรม D2", "aboutDescription": "กรุณาเรนเดอร์เฉพาะไดอะแกรม D2 เป็นโปรแกรมดูและเรนเดอร์ไดอะแกรม D2 แบบออฟไลน์ที่เรียบง่าย", "layout": "เค้าโครง", "sketch": "สเก็ตช์", "ready": "พร้อม", "rendering": "กำลังเรนเดอร์ D2…", "empty": "ซอร์ส D2 ว่าง", "invalid": "ซอร์ส D2 ไม่ถูกต้อง", "d2Initializing": "กำลังเริ่มต้น D2…", "d2CompileError": "D2 ไม่สามารถคอมไพล์ซอร์สได้", "d2RenderError": "D2 ไม่สามารถเรนเดอร์ไดอะแกรมได้", "d2ImportUnsupported": "ตัวแก้ไขไฟล์เดียวนี้ไม่รองรับการ import ของ D2 ให้วาง D2 ที่ต้องนำเข้าในตัวแก้ไข", "d2ExternalResource": "รูปภาพหรือไอคอนภายนอกหรือไฟล์ข้างเคียงถูกบล็อก ใช้ URI data:image แบบฝังแทน", "sourceEditor": "ตัวแก้ไขซอร์ส D2", "sourceText": "ข้อความซอร์ส D2", "renderedPreview": "ตัวอย่างไดอะแกรม D2 ที่เรนเดอร์แล้ว", "previewFrameTitle": "ไดอะแกรม D2 ที่เรนเดอร์แล้ว", "layoutAria": "เอนจินเค้าโครง D2", "sketchAria": "โหมดสเก็ตช์ D2" }, "fa": { "title": "لطفاً فقط نمودار D2 را رندر کن", "aboutDescription": "لطفاً فقط نمودار D2 را رندر کن یک نمایشگر و رندرکنندهٔ ساده و آفلاین برای نمودارهای D2 است.", "layout": "چیدمان", "sketch": "طرح دستی", "ready": "آماده.", "rendering": "در حال رندر D2…", "empty": "منبع D2 خالی است.", "invalid": "منبع D2 نامعتبر است.", "d2Initializing": "در حال راه‌اندازی D2…", "d2CompileError": "D2 نتوانست منبع را کامپایل کند.", "d2RenderError": "D2 نتوانست نمودار را رندر کند.", "d2ImportUnsupported": "واردکردن D2 در این ویرایشگر تک‌فایلی در دسترس نیست. D2 واردشونده را در ویرایشگر جای‌گذاری کنید.", "d2ExternalResource": "تصاویر یا آیکون‌های خارجی یا فایل‌های کنار آن مسدود هستند. از URI تعبیه‌شدهٔ data:image استفاده کنید.", "sourceEditor": "ویرایشگر منبع D2", "sourceText": "متن منبع D2", "renderedPreview": "پیش‌نمایش نمودار D2 رندرشده", "previewFrameTitle": "نمودار D2 رندرشده", "layoutAria": "موتور چیدمان D2", "sketchAria": "حالت طرح دستی D2" }, "uk": { "title": "Будь ласка, просто відтворіть діаграму D2", "aboutDescription": "Будь ласка, просто відтворіть діаграму D2 — мінімальний автономний переглядач і рендерер діаграм D2.", "layout": "Компонування", "sketch": "Ескіз", "ready": "Готово.", "rendering": "Відтворення D2…", "empty": "Порожній вихідний текст D2.", "invalid": "Некоректний вихідний текст D2.", "d2Initializing": "Ініціалізація D2…", "d2CompileError": "D2 не вдалося скомпілювати вихідний текст.", "d2RenderError": "D2 не вдалося відтворити діаграму.", "d2ImportUnsupported": "Імпорти D2 недоступні в цьому однофайловому редакторі. Вставте імпортований D2 у редактор.", "d2ExternalResource": "Зовнішні або сусідні зображення/піктограми заблоковано. Використовуйте вбудований URI data:image.", "sourceEditor": "Редактор вихідного D2", "sourceText": "Вихідний текст D2", "renderedPreview": "Попередній перегляд відтвореної діаграми D2", "previewFrameTitle": "Відтворена діаграма D2", "layoutAria": "Рушій компонування D2", "sketchAria": "Режим ескізу D2" }, "cs": { "title": "Prosím, jen vykreslete diagram D2", "aboutDescription": "Prosím, jen vykreslete diagram D2 je minimální offline prohlížeč a vykreslovač diagramů D2.", "layout": "Rozvržení", "sketch": "Náčrt", "ready": "Připraveno.", "rendering": "Vykresluje se D2…", "empty": "Prázdný zdroj D2.", "invalid": "Neplatný zdroj D2.", "d2Initializing": "Inicializuje se D2…", "d2CompileError": "D2 nemohl zkompilovat zdroj.", "d2RenderError": "D2 nemohl vykreslit diagram.", "d2ImportUnsupported": "Importy D2 nejsou v tomto jednosouborovém editoru dostupné. Vložte importovaný D2 do editoru.", "d2ExternalResource": "Externí nebo sousední obrázky/ikony jsou blokovány. Použijte vložené URI data:image.", "sourceEditor": "Editor zdroje D2", "sourceText": "Zdrojový text D2", "renderedPreview": "Náhled vykresleného diagramu D2", "previewFrameTitle": "Vykreslený diagram D2", "layoutAria": "Engine rozvržení D2", "sketchAria": "Režim náčrtu D2" }, "ms": { "title": "Sila Render Rajah D2 Sahaja", "aboutDescription": "Sila Render Rajah D2 Sahaja ialah pemapar dan perender rajah D2 luar talian yang minimum.", "layout": "Susun atur", "sketch": "Lakaran", "ready": "Sedia.", "rendering": "Merender D2…", "empty": "Sumber D2 kosong.", "invalid": "Sumber D2 tidak sah.", "d2Initializing": "Memulakan D2…", "d2CompileError": "D2 tidak dapat menyusun sumber.", "d2RenderError": "D2 tidak dapat merender rajah.", "d2ImportUnsupported": "Import D2 tidak tersedia dalam editor satu fail ini. Tampal D2 yang diimport ke dalam editor.", "d2ExternalResource": "Imej atau ikon luaran atau bersebelahan disekat. Gunakan URI data:image terbenam.", "sourceEditor": "Editor sumber D2", "sourceText": "Teks sumber D2", "renderedPreview": "Pratonton rajah D2 yang dirender", "previewFrameTitle": "Rajah D2 yang dirender", "layoutAria": "Enjin susun atur D2", "sketchAria": "Mod lakaran D2" }, "ro": { "title": "Redați doar diagrama D2, vă rog", "aboutDescription": "Redați doar diagrama D2, vă rog este un vizualizator și renderer D2 minimal, offline.", "layout": "Aspect", "sketch": "Schiță", "ready": "Gata.", "rendering": "Se redă D2…", "empty": "Sursă D2 goală.", "invalid": "Sursă D2 nevalidă.", "d2Initializing": "Se inițializează D2…", "d2CompileError": "D2 nu a putut compila sursa.", "d2RenderError": "D2 nu a putut reda diagrama.", "d2ImportUnsupported": "Importurile D2 nu sunt disponibile în acest editor cu un singur fișier. Lipiți D2 importat în editor.", "d2ExternalResource": "Imaginile sau pictogramele externe ori din fișiere alăturate sunt blocate. Folosiți un URI data:image încorporat.", "sourceEditor": "Editor sursă D2", "sourceText": "Text sursă D2", "renderedPreview": "Previzualizare diagramă D2 redată", "previewFrameTitle": "Diagramă D2 redată", "layoutAria": "Motor de aspect D2", "sketchAria": "Mod schiță D2" }, "el": { "title": "Απλώς αποδώστε το διάγραμμα D2, παρακαλώ", "aboutDescription": "Απλώς αποδώστε το διάγραμμα D2, παρακαλώ είναι ένα ελάχιστο, εκτός σύνδεσης πρόγραμμα προβολής και απόδοσης διαγραμμάτων D2.", "layout": "Διάταξη", "sketch": "Σκίτσο", "ready": "Έτοιμο.", "rendering": "Απόδοση D2…", "empty": "Κενή πηγή D2.", "invalid": "Μη έγκυρη πηγή D2.", "d2Initializing": "Αρχικοποίηση D2…", "d2CompileError": "Το D2 δεν μπόρεσε να μεταγλωττίσει την πηγή.", "d2RenderError": "Το D2 δεν μπόρεσε να αποδώσει το διάγραμμα.", "d2ImportUnsupported": "Οι εισαγωγές D2 δεν είναι διαθέσιμες σε αυτόν τον επεξεργαστή ενός αρχείου. Επικολλήστε το εισαγόμενο D2 στον επεξεργαστή.", "d2ExternalResource": "Εξωτερικές ή γειτονικές εικόνες/εικονίδια αποκλείονται. Χρησιμοποιήστε ενσωματωμένο URI data:image.", "sourceEditor": "Επεξεργαστής πηγής D2", "sourceText": "Κείμενο πηγής D2", "renderedPreview": "Προεπισκόπηση αποδομένου διαγράμματος D2", "previewFrameTitle": "Αποδομένο διάγραμμα D2", "layoutAria": "Μηχανή διάταξης D2", "sketchAria": "Λειτουργία σκίτσου D2" }, "he": { "title": "רק לרנדר את דיאגרמת D2, בבקשה", "aboutDescription": "רק לרנדר את דיאגרמת D2, בבקשה הוא מציג ומרנדר מינימלי ולא מקוון לדיאגרמות D2.", "layout": "פריסה", "sketch": "סקיצה", "ready": "מוכן.", "rendering": "מרנדר D2…", "empty": "מקור D2 ריק.", "invalid": "מקור D2 לא תקין.", "d2Initializing": "מאתחל D2…", "d2CompileError": "D2 לא הצליח להדר את המקור.", "d2RenderError": "D2 לא הצליח לרנדר את הדיאגרמה.", "d2ImportUnsupported": "ייבואי D2 אינם זמינים בעורך הקובץ היחיד הזה. הדבק את ה-D2 המיובא בעורך.", "d2ExternalResource": "תמונות או סמלים חיצוניים או מקבצים סמוכים חסומים. השתמש ב-URI מוטמע מסוג data:image.", "sourceEditor": "עורך מקור D2", "sourceText": "טקסט מקור D2", "renderedPreview": "תצוגה מקדימה של דיאגרמת D2 מרונדרת", "previewFrameTitle": "דיאגרמת D2 מרונדרת", "layoutAria": "מנוע פריסה D2", "sketchAria": "מצב סקיצה D2" }, "sv": { "title": "Rendera bara D2-diagrammet, tack", "aboutDescription": "Rendera bara D2-diagrammet, tack är en minimal offlinevisare och renderer för D2-diagram.", "layout": "Layout", "sketch": "Skiss", "ready": "Klar.", "rendering": "Renderar D2…", "empty": "Tom D2-källa.", "invalid": "Ogiltig D2-källa.", "d2Initializing": "Initierar D2…", "d2CompileError": "D2 kunde inte kompilera källan.", "d2RenderError": "D2 kunde inte rendera diagrammet.", "d2ImportUnsupported": "D2-importer är inte tillgängliga i den här enfilsredigeraren. Klistra in importerad D2 i redigeraren.", "d2ExternalResource": "Externa eller intilliggande bilder/ikoner blockeras. Använd en inbäddad data:image-URI.", "sourceEditor": "D2-källredigerare", "sourceText": "D2-källtext", "renderedPreview": "Förhandsvisning av renderat D2-diagram", "previewFrameTitle": "Renderat D2-diagram", "layoutAria": "D2-layoutmotor", "sketchAria": "D2-skissläge" }, "fil": { "title": "I-render lang ang D2 diagram, pakiusap", "aboutDescription": "I-render lang ang D2 diagram, pakiusap ay isang minimal at offline na D2 diagram viewer at renderer.", "layout": "Layout", "sketch": "Sketch", "ready": "Handa na.", "rendering": "Nagre-render ng D2…", "empty": "Walang laman ang D2 source.", "invalid": "Hindi wasto ang D2 source.", "d2Initializing": "Sinisimulan ang D2…", "d2CompileError": "Hindi ma-compile ng D2 ang source.", "d2RenderError": "Hindi ma-render ng D2 ang diagram.", "d2ImportUnsupported": "Hindi available ang D2 imports sa single-file editor na ito. I-paste ang ini-import na D2 sa editor.", "d2ExternalResource": "Naka-block ang panlabas o katabing mga larawan/icon. Gumamit ng naka-embed na data:image URI.", "sourceEditor": "D2 source editor", "sourceText": "D2 source text", "renderedPreview": "Preview ng na-render na D2 diagram", "previewFrameTitle": "Na-render na D2 diagram", "layoutAria": "D2 layout engine", "sketchAria": "D2 sketch mode" }, "ta": { "title": "தயவுசெய்து D2 வரைபடத்தை மட்டும் ரெண்டர் செய்யவும்", "aboutDescription": "தயவுசெய்து D2 வரைபடத்தை மட்டும் ரெண்டர் செய்யவும் என்பது குறைந்தபட்ச ஆஃப்லைன் D2 வரைபடக் காட்சி மற்றும் ரெண்டரர் ஆகும்.", "layout": "அமைப்பு", "sketch": "வரைவு", "ready": "தயார்.", "rendering": "D2 ரெண்டர் செய்யப்படுகிறது…", "empty": "D2 மூலம் காலியாக உள்ளது.", "invalid": "D2 மூலம் தவறானது.", "d2Initializing": "D2 தொடங்கப்படுகிறது…", "d2CompileError": "D2 மூலத்தை கம்பைல் செய்ய முடியவில்லை.", "d2RenderError": "D2 வரைபடத்தை ரெண்டர் செய்ய முடியவில்லை.", "d2ImportUnsupported": "இந்த ஒற்றை-கோப்பு திருத்தியில் D2 இறக்குமதிகள் கிடைக்காது. இறக்குமதி செய்யும் D2 ஐ திருத்தியில் ஒட்டவும்.", "d2ExternalResource": "வெளிப்புற அல்லது அடுத்துள்ள படம்/ஐகான்கள் தடுக்கப்பட்டுள்ளன. உட்பொதிக்கப்பட்ட data:image URI ஐ பயன்படுத்தவும்.", "sourceEditor": "D2 மூலம் திருத்தி", "sourceText": "D2 மூலம் உரை", "renderedPreview": "ரெண்டர் செய்யப்பட்ட D2 வரைபட முன்னோட்டம்", "previewFrameTitle": "ரெண்டர் செய்யப்பட்ட D2 வரைபடம்", "layoutAria": "D2 அமைப்பு இயந்திரம்", "sketchAria": "D2 வரைவு முறை" }, "hu": { "title": "Kérem, csak a D2 diagramot renderelje", "aboutDescription": "Kérem, csak a D2 diagramot renderelje egy minimális, offline D2 diagramnézegető és renderelő.", "layout": "Elrendezés", "sketch": "Vázlat", "ready": "Kész.", "rendering": "D2 renderelése…", "empty": "Üres D2-forrás.", "invalid": "Érvénytelen D2-forrás.", "d2Initializing": "D2 inicializálása…", "d2CompileError": "A D2 nem tudta lefordítani a forrást.", "d2RenderError": "A D2 nem tudta renderelni a diagramot.", "d2ImportUnsupported": "A D2-importok nem érhetők el ebben az egyfájlos szerkesztőben. Illessze be az importált D2-t a szerkesztőbe.", "d2ExternalResource": "A külső vagy szomszédos képek/ikonok blokkolva vannak. Használjon beágyazott data:image URI-t.", "sourceEditor": "D2-forrásszerkesztő", "sourceText": "D2-forrásszöveg", "renderedPreview": "Renderelt D2 diagram előnézete", "previewFrameTitle": "Renderelt D2 diagram", "layoutAria": "D2 elrendezési motor", "sketchAria": "D2 vázlat mód" }, "da": { "title": "Render kun D2-diagrammet, tak", "aboutDescription": "Render kun D2-diagrammet, tak er en minimal offline D2-diagramfremviser og -renderer.", "layout": "Layout", "sketch": "Skitse", "ready": "Klar.", "rendering": "Renderer D2…", "empty": "Tom D2-kilde.", "invalid": "Ugyldig D2-kilde.", "d2Initializing": "Initialiserer D2…", "d2CompileError": "D2 kunne ikke kompilere kilden.", "d2RenderError": "D2 kunne ikke rendere diagrammet.", "d2ImportUnsupported": "D2-importer er ikke tilgængelige i denne enkeltfilseditor. Indsæt den importerede D2 i editoren.", "d2ExternalResource": "Eksterne eller tilstødende billeder/ikoner er blokeret. Brug en indlejret data:image-URI.", "sourceEditor": "D2-kildeeditor", "sourceText": "D2-kildetekst", "renderedPreview": "Forhåndsvisning af renderet D2-diagram", "previewFrameTitle": "Renderet D2-diagram", "layoutAria": "D2-layoutmotor", "sketchAria": "D2-skitsetilstand" }, "fi": { "title": "Renderöi vain D2-kaavio, kiitos", "aboutDescription": "Renderöi vain D2-kaavio, kiitos on minimaalinen offline D2-kaavioiden katselu- ja renderöintityökalu.", "layout": "Asettelu", "sketch": "Luonnos", "ready": "Valmis.", "rendering": "Renderöidään D2…", "empty": "Tyhjä D2-lähde.", "invalid": "Virheellinen D2-lähde.", "d2Initializing": "Alustetaan D2…", "d2CompileError": "D2 ei voinut kääntää lähdettä.", "d2RenderError": "D2 ei voinut renderöidä kaaviota.", "d2ImportUnsupported": "D2-tuonnit eivät ole käytettävissä tässä yhden tiedoston editorissa. Liitä tuotava D2 editoriin.", "d2ExternalResource": "Ulkoiset tai viereiset kuvat/kuvakkeet on estetty. Käytä upotettua data:image-URIa.", "sourceEditor": "D2-lähde-editori", "sourceText": "D2-lähdeteksti", "renderedPreview": "Renderöidyn D2-kaavion esikatselu", "previewFrameTitle": "Renderöity D2-kaavio", "layoutAria": "D2-asettelumoottori", "sketchAria": "D2-luonnostila" }, "nb": { "title": "Bare render D2-diagrammet, takk", "aboutDescription": "Bare render D2-diagrammet, takk er en minimal frakoblet D2-diagramviser og -renderer.", "layout": "Oppsett", "sketch": "Skisse", "ready": "Klar.", "rendering": "Renderer D2…", "empty": "Tom D2-kilde.", "invalid": "Ugyldig D2-kilde.", "d2Initializing": "Initialiserer D2…", "d2CompileError": "D2 kunne ikke kompilere kilden.", "d2RenderError": "D2 kunne ikke rendere diagrammet.", "d2ImportUnsupported": "D2-importer er ikke tilgjengelige i denne enkeltfilredigereren. Lim inn importert D2 i redigereren.", "d2ExternalResource": "Eksterne eller tilstøtende bilder/ikoner blokkeres. Bruk en innebygd data:image-URI.", "sourceEditor": "D2-kilderedigerer", "sourceText": "D2-kildetekst", "renderedPreview": "Forhåndsvisning av rendert D2-diagram", "previewFrameTitle": "Rendert D2-diagram", "layoutAria": "D2-oppsettsmotor", "sketchAria": "D2-skissemodus" }, "sk": { "title": "Prosím, iba vykreslite diagram D2", "aboutDescription": "Prosím, iba vykreslite diagram D2 je minimálny offline prehliadač a vykresľovač diagramov D2.", "layout": "Rozloženie", "sketch": "Náčrt", "ready": "Pripravené.", "rendering": "Vykresľuje sa D2…", "empty": "Prázdny zdroj D2.", "invalid": "Neplatný zdroj D2.", "d2Initializing": "Inicializuje sa D2…", "d2CompileError": "D2 nedokázal skompilovať zdroj.", "d2RenderError": "D2 nedokázal vykresliť diagram.", "d2ImportUnsupported": "Importy D2 nie sú v tomto jednosúborovom editore dostupné. Vložte importovaný D2 do editora.", "d2ExternalResource": "Externé alebo susedné obrázky/ikony sú blokované. Použite vložené URI data:image.", "sourceEditor": "Editor zdroja D2", "sourceText": "Zdrojový text D2", "renderedPreview": "Náhľad vykresleného diagramu D2", "previewFrameTitle": "Vykreslený diagram D2", "layoutAria": "Engine rozloženia D2", "sketchAria": "Režim náčrtu D2" }, "bg": { "title": "Моля, просто визуализирайте D2 диаграмата", "aboutDescription": "Моля, просто визуализирайте D2 диаграмата е минимален офлайн преглед и рендерер за D2 диаграми.", "layout": "Оформление", "sketch": "Скица", "ready": "Готово.", "rendering": "Визуализиране на D2…", "empty": "Празен D2 изходен текст.", "invalid": "Невалиден D2 изходен текст.", "d2Initializing": "Инициализиране на D2…", "d2CompileError": "D2 не успя да компилира изходния текст.", "d2RenderError": "D2 не успя да визуализира диаграмата.", "d2ImportUnsupported": "D2 импортите не са достъпни в този еднофайлов редактор. Поставете импортирания D2 в редактора.", "d2ExternalResource": "Външни или съседни изображения/икони са блокирани. Използвайте вграден data:image URI.", "sourceEditor": "Редактор на D2 изходен текст", "sourceText": "D2 изходен текст", "renderedPreview": "Преглед на визуализираната D2 диаграма", "previewFrameTitle": "Визуализирана D2 диаграма", "layoutAria": "D2 двигател за оформление", "sketchAria": "D2 режим скица" }, "sr": { "title": "Молим, само прикажите D2 дијаграм", "aboutDescription": "Молим, само прикажите D2 дијаграм је минималан офлајн прегледач и рендерер D2 дијаграма.", "layout": "Распоред", "sketch": "Скица", "ready": "Спремно.", "rendering": "Приказивање D2…", "empty": "Празан D2 извор.", "invalid": "Неисправан D2 извор.", "d2Initializing": "Покретање D2…", "d2CompileError": "D2 није могао да компајлира извор.", "d2RenderError": "D2 није могао да прикаже дијаграм.", "d2ImportUnsupported": "D2 увози нису доступни у овом уређивачу једне датотеке. Налепите увезени D2 у уређивач.", "d2ExternalResource": "Спољне или суседне слике/иконе су блокиране. Користите уграђени data:image URI.", "sourceEditor": "Уређивач D2 извора", "sourceText": "D2 изворни текст", "renderedPreview": "Преглед приказаног D2 дијаграма", "previewFrameTitle": "Приказани D2 дијаграм", "layoutAria": "D2 мотор распореда", "sketchAria": "D2 режим скице" }, "hr": { "title": "Molim, samo prikažite D2 dijagram", "aboutDescription": "Molim, samo prikažite D2 dijagram minimalan je izvanmrežni preglednik i renderer D2 dijagrama.", "layout": "Raspored", "sketch": "Skica", "ready": "Spremno.", "rendering": "Prikazivanje D2…", "empty": "Prazan D2 izvor.", "invalid": "Neispravan D2 izvor.", "d2Initializing": "Pokretanje D2…", "d2CompileError": "D2 nije mogao kompilirati izvor.", "d2RenderError": "D2 nije mogao prikazati dijagram.", "d2ImportUnsupported": "D2 uvozi nisu dostupni u ovom uređivaču jedne datoteke. Zalijepite uvezeni D2 u uređivač.", "d2ExternalResource": "Vanjske ili susjedne slike/ikone su blokirane. Upotrijebite ugrađeni data:image URI.", "sourceEditor": "Uređivač D2 izvora", "sourceText": "D2 izvorni tekst", "renderedPreview": "Pregled prikazanog D2 dijagrama", "previewFrameTitle": "Prikazani D2 dijagram", "layoutAria": "D2 mehanizam rasporeda", "sketchAria": "D2 način skice" }, "sl": { "title": "Prosim, izrišite samo diagram D2", "aboutDescription": "Prosim, izrišite samo diagram D2 je minimalen pregledovalnik in izrisovalnik diagramov D2 brez povezave.", "layout": "Postavitev", "sketch": "Skica", "ready": "Pripravljeno.", "rendering": "Izrisovanje D2…", "empty": "Prazen vir D2.", "invalid": "Neveljaven vir D2.", "d2Initializing": "Inicializacija D2…", "d2CompileError": "D2 ni mogel prevesti vira.", "d2RenderError": "D2 ni mogel izrisati diagrama.", "d2ImportUnsupported": "Uvozi D2 v tem urejevalniku ene datoteke niso na voljo. Uvoženi D2 prilepite v urejevalnik.", "d2ExternalResource": "Zunanje ali sosednje slike/ikone so blokirane. Uporabite vdelani URI data:image.", "sourceEditor": "Urejevalnik vira D2", "sourceText": "Izvorno besedilo D2", "renderedPreview": "Predogled izrisanega diagrama D2", "previewFrameTitle": "Izrisan diagram D2", "layoutAria": "Mehanizem postavitve D2", "sketchAria": "Način skice D2" }, "ca": { "title": "Renderitza només el diagrama D2, si us plau", "aboutDescription": "Renderitza només el diagrama D2, si us plau és un visor i renderitzador D2 mínim i fora de línia.", "layout": "Disposició", "sketch": "Esbós", "ready": "A punt.", "rendering": "Renderitzant D2…", "empty": "Font D2 buida.", "invalid": "Font D2 no vàlida.", "d2Initializing": "Inicialitzant D2…", "d2CompileError": "D2 no ha pogut compilar la font.", "d2RenderError": "D2 no ha pogut renderitzar el diagrama.", "d2ImportUnsupported": "Les importacions D2 no estan disponibles en aquest editor d’un sol fitxer. Enganxa el D2 importat a l’editor.", "d2ExternalResource": "Les imatges o icones externes o adjacents estan bloquejades. Utilitza un URI data:image incrustat.", "sourceEditor": "Editor de font D2", "sourceText": "Text font D2", "renderedPreview": "Previsualització del diagrama D2 renderitzat", "previewFrameTitle": "Diagrama D2 renderitzat", "layoutAria": "Motor de disposició D2", "sketchAria": "Mode d’esbós D2" } }; for (const [code, strings] of Object.entries(D2_TRANSLATIONS)) { Object.assign(TRANSLATIONS[code], strings); } const D2_RUNTIME_FAILED_TRANSLATIONS = {"en": "D2 runtime failed to initialize.", "zh-CN": "D2 运行时初始化失败。", "es": "No se pudo inicializar el entorno D2.", "ar": "فشل تهيئة بيئة تشغيل D2.", "id": "Runtime D2 gagal diinisialisasi.", "pt": "Falha ao inicializar o runtime D2.", "fr": "Échec de l’initialisation du moteur D2.", "ja": "D2 ランタイムの初期化に失敗しました。", "ru": "Не удалось инициализировать среду D2.", "de": "Die D2-Laufzeit konnte nicht initialisiert werden.", "hi": "D2 रनटाइम आरंभ नहीं हो सका।", "bn": "D2 রানটাইম শুরু করা যায়নি।", "ur": "D2 رن ٹائم شروع نہیں ہو سکا۔", "ko": "D2 런타임을 초기화하지 못했습니다.", "vi": "Không thể khởi tạo môi trường D2.", "tr": "D2 çalışma zamanı başlatılamadı.", "it": "Impossibile inizializzare il runtime D2.", "nl": "De D2-runtime kon niet worden geïnitialiseerd.", "pl": "Nie udało się zainicjować środowiska D2.", "th": "ไม่สามารถเริ่มต้นรันไทม์ D2 ได้", "fa": "راه‌اندازی محیط اجرای D2 ناموفق بود.", "uk": "Не вдалося ініціалізувати середовище D2.", "cs": "Běhové prostředí D2 se nepodařilo inicializovat.", "ms": "Runtime D2 gagal dimulakan.", "ro": "Runtime-ul D2 nu a putut fi inițializat.", "el": "Αποτυχία αρχικοποίησης του περιβάλλοντος D2.", "he": "אתחול סביבת D2 נכשל.", "sv": "D2-körmiljön kunde inte initieras.", "fil": "Hindi ma-initialize ang D2 runtime.", "ta": "D2 இயக்கநேரத்தை தொடங்க முடியவில்லை.", "hu": "A D2 futtatókörnyezet inicializálása sikertelen.", "da": "D2-runtime kunne ikke initialiseres.", "fi": "D2-ajoympäristön alustus epäonnistui.", "nb": "D2-kjøremiljøet kunne ikke initialiseres.", "sk": "Behové prostredie D2 sa nepodarilo inicializovať.", "bg": "Средата D2 не можа да се инициализира.", "sr": "D2 окружење није могло да се покрене.", "hr": "D2 okruženje nije se moglo pokrenuti.", "sl": "Izvajalnega okolja D2 ni bilo mogoče inicializirati.", "ca": "No s’ha pogut inicialitzar l’entorn D2."}; for (const [code, message] of Object.entries(D2_RUNTIME_FAILED_TRANSLATIONS)) { TRANSLATIONS[code].d2RuntimeFailed = message; } const ENGLISH_TRANSLATION_KEYS = Object.keys(TRANSLATIONS.en); for (const strings of Object.values(TRANSLATIONS)) { for (const key of ENGLISH_TRANSLATION_KEYS) { if (!Object.prototype.hasOwnProperty.call(strings, key)) strings[key] = TRANSLATIONS.en[key]; } } // DO NOT DELETE CODE BELOW THIS LINE. const LANGUAGE_META = { "en": { "label": "🇺🇸 English", "dir": "ltr" }, "zh-CN": { "label": "🇨🇳 简体中文", "dir": "ltr" }, "es": { "label": "🇪🇸 Español", "dir": "ltr" }, "ar": { "label": "🇸🇦 العربية", "dir": "rtl" }, "id": { "label": "🇮🇩 Bahasa Indonesia", "dir": "ltr" }, "pt": { "label": "🇵🇹 Português", "dir": "ltr" }, "fr": { "label": "🇫🇷 Français", "dir": "ltr" }, "ja": { "label": "🇯🇵 日本語", "dir": "ltr" }, "ru": { "label": "🇷🇺 Русский", "dir": "ltr" }, "de": { "label": "🇩🇪 Deutsch", "dir": "ltr" }, "hi": { "label": "🇮🇳 हिन्दी", "dir": "ltr" }, "bn": { "label": "🇧🇩 বাংলা", "dir": "ltr" }, "ur": { "label": "🇵🇰 اردو", "dir": "rtl" }, "ko": { "label": "🇰🇷 한국어", "dir": "ltr" }, "vi": { "label": "🇻🇳 Tiếng Việt", "dir": "ltr" }, "tr": { "label": "🇹🇷 Türkçe", "dir": "ltr" }, "it": { "label": "🇮🇹 Italiano", "dir": "ltr" }, "nl": { "label": "🇳🇱 Nederlands", "dir": "ltr" }, "pl": { "label": "🇵🇱 Polski", "dir": "ltr" }, "th": { "label": "🇹🇭 ไทย", "dir": "ltr" }, "fa": { "label": "🇮🇷 فارسی", "dir": "rtl" }, "uk": { "label": "🇺🇦 Українська", "dir": "ltr" }, "cs": { "label": "🇨🇿 Čeština", "dir": "ltr" }, "ms": { "label": "🇲🇾 Bahasa Melayu", "dir": "ltr" }, "ro": { "label": "🇷🇴 Română", "dir": "ltr" }, "el": { "label": "🇬🇷 Ελληνικά", "dir": "ltr" }, "he": { "label": "🇮🇱 עברית", "dir": "rtl" }, "sv": { "label": "🇸🇪 Svenska", "dir": "ltr" }, "fil": { "label": "🇵🇭 Filipino", "dir": "ltr" }, "ta": { "label": "🇮🇳 தமிழ்", "dir": "ltr" }, "hu": { "label": "🇭🇺 Magyar", "dir": "ltr" }, "da": { "label": "🇩🇰 Dansk", "dir": "ltr" }, "fi": { "label": "🇫🇮 Suomi", "dir": "ltr" }, "nb": { "label": "🇳🇴 Norsk bokmål", "dir": "ltr" }, "sk": { "label": "🇸🇰 Slovenčina", "dir": "ltr" }, "bg": { "label": "🇧🇬 Български", "dir": "ltr" }, "sr": { "label": "🇷🇸 Српски", "dir": "ltr" }, "hr": { "label": "🇭🇷 Hrvatski", "dir": "ltr" }, "sl": { "label": "🇸🇮 Slovenščina", "dir": "ltr" }, "ca": { "label": "🇪🇸 Català", "dir": "ltr" } }; /* ============================================================================ EXAMPLES — SECOND COMMON PLACE TO EDIT rendererOptions is intentionally part of every example. A Graphviz fork can use { engine: "neato" }; a chart app might use { theme: "dark" }, etc. The framework guarantees a trailing newline when an example is loaded. ============================================================================ */ const EXAMPLES = Object.freeze([ Object.freeze({ id: "simple-connections", label: "Simple Connections", source: `browser: Browser api: API store: Data Store browser -> api: HTTPS api -> store: query store -> api: rows `, rendererOptions: Object.freeze({ layout: "dagre", sketch: false }) }), Object.freeze({ id: "containers", label: "Containers", source: `frontend: Frontend { browser: Browser app: Web App browser -> app } backend: Backend { api: API worker: Worker db: Database { shape: cylinder } api -> worker: enqueue worker -> db: write } frontend.app -> backend.api: request `, rendererOptions: Object.freeze({ layout: "dagre", sketch: false }) }), Object.freeze({ id: "architecture", label: "Architecture", source: `direction: right client: Client gateway: API Gateway services: Services { accounts: Accounts billing: Billing notifications: Notifications } data: Data { primary: Primary DB { shape: cylinder } events: Event Log { shape: queue } } client -> gateway gateway -> services.accounts gateway -> services.billing services.accounts -> data.primary services.billing -> data.primary services.billing -> data.events data.events -> services.notifications `, rendererOptions: Object.freeze({ layout: "elk", sketch: false }) }), Object.freeze({ id: "sequence-diagram", label: "Sequence Diagram", source: `shape: sequence_diagram user: User web: Web App api: API user -> web: Submit form web -> api: POST /items api -> web: 201 Created web -> user: Show success `, rendererOptions: Object.freeze({ layout: "dagre", sketch: false }) }), Object.freeze({ id: "classes", label: "Classes", source: `User: { shape: class +id: string +email: string +signIn(password string): bool } Admin: { shape: class +role: string +disableUser(id string): bool } Admin -> User: extends { target-arrowhead.shape: triangle target-arrowhead.style.filled: false } `, rendererOptions: Object.freeze({ layout: "dagre", sketch: false }) }), Object.freeze({ id: "styled-diagram", label: "Styled Diagram", source: `direction: right start: Start { shape: oval style.fill: "#dbeafe" style.stroke: "#2563eb" } work: Process { style.fill: "#dcfce7" style.stroke: "#16a34a" style.shadow: true } decision: Ready? { shape: diamond style.fill: "#fef3c7" style.stroke: "#d97706" } done: Done { shape: oval style.fill: "#f3e8ff" style.stroke: "#9333ea" } start -> work work -> decision decision -> done: yes `, rendererOptions: Object.freeze({ layout: "dagre", sketch: true }) }) ]); /* ============================================================================ DEVELOPMENT TEST IMAGE This is deliberately renderer-independent. With APP.development.showTemplateTestImage=true the template always displays this SVG regardless of editor contents. That makes pan, zoom, Fit, SVG export, PNG/WebP export, and Export Scale testable before a real renderer exists. A real fork should normally set showTemplateTestImage=false and implement RENDERER.render(). ============================================================================ */ const TEMPLATE_TEST_RENDER = Object.freeze({ svg: ` Template Preview Pan • Zoom • Fit • Export `, width: 640, height: 400 }); /* ============================================================================ RENDERER INTERFACE — PRIMARY APP-SPECIFIC EXTENSION POINT A fork should replace this object rather than editing generic UI functions. Required/expected hooks: validate(source, options, context) -> { valid: true } OR { valid: false, error: "..." } async render(source, options, context) -> { svg: "", width?: number, height?: number } -> null when there intentionally is no render tidy(source, options, context) -> formatted source string Optional hooks: initialize(context) one-time renderer startup installControls(container, context) renderer-specific controls slot installExports(container, context) renderer-specific export buttons slot applyExampleOptions(options, context) react to example metadata SECURITY RULE: Renderer output MUST pass through acceptRender(), which sanitizes the SVG and puts it in a sandboxed iframe. Renderer code should never inject generated markup into the main document. ============================================================================ */ /* D2 RENDERER LIFECYCLE --------------------- @d2lang/d2 0.1.33 exposes compile() and render() in its public JavaScript API; it does not expose the CLI's official `d2 fmt` formatter. We therefore create one D2 instance here, reuse it for the lifetime of the page, and keep Tidy intentionally conservative below. Dagre and ELK are the two browser/WASM layouts exposed by the package. TALA is deliberately absent: it is not available in this browser/WASM renderer and would require functionality outside this fully offline app. Imports are not silently fetched. This one-editor application has no UI for an in-memory multi-file fs map, so D2 @imports are rejected with a useful message. Network/sibling image and icon references are likewise rejected; only data:image content may be embedded. The sanitizer separately strips any external reference that survives compilation, providing defense in depth. */ let d2Runtime = null; let d2RenderQueue = Promise.resolve(); const d2ControlElements = { layout: null, sketch: null, layoutLabel: null, sketchLabel: null }; function queueD2Work(job) { const run = d2RenderQueue.then(job, job); // Keep the queue alive after failures while returning the real result/error to // the caller. This also guarantees the package never has concurrent requests. d2RenderQueue = run.catch(() => undefined); return run; } function formatD2Failure(error, context, fallbackKey) { const raw = error?.message || String(error || ""); let details = []; try { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) { details = parsed.map(item => { if (item && typeof item === "object") { const message = item.errmsg || item.message || JSON.stringify(item); const range = item.range && !String(message).includes(String(item.range)) ? ` (${item.range})` : ""; return `${message}${range}`; } return String(item); }); } } catch { // D2 sometimes returns a normal Error string rather than its parser JSON array. } if (!details.length && raw) details = [raw]; return [context.t(fallbackKey), ...details].filter(Boolean).join("\n"); } function hasUnsupportedImport(source) { // D2 import forms include `x: @file` and spread imports such as `...@models`. // Requiring whitespace before @ avoids false positives for ordinary email text. return /(^|\s)(?:\.\.\.\s*)?@[A-Za-z0-9_.\/-]+/m.test(source); } function hasUnsupportedExternalResource(source) { // D2 images/icons are URLs assigned to `icon:` (including dotted x.icon:). // Allow only inline data:image values. Also cover Markdown/raw-HTML images. const iconPattern = /(?:^|\n)\s*(?:[A-Za-z0-9_."' -]+\.)?icon\s*:\s*([^\n#]+)/gi; for (const match of source.matchAll(iconPattern)) { const value = match[1].trim().replace(/^['"]|['"]$/g, ""); if (value && !/^data:image\//i.test(value)) return true; } const markdownImages = /!\[[^\]]*\]\(\s*([^\s)]+)[^)]*\)/gi; for (const match of source.matchAll(markdownImages)) { if (!/^data:image\//i.test(match[1])) return true; } const htmlImages = /]*\bsrc\s*=\s*["']?([^\s"'>]+)/gi; for (const match of source.matchAll(htmlImages)) { if (!/^data:image\//i.test(match[1])) return true; } return false; } function syncD2Controls(context) { const options = context.getRendererOptions(); if (d2ControlElements.layout) d2ControlElements.layout.value = options.layout || "dagre"; if (d2ControlElements.sketch) d2ControlElements.sketch.checked = Boolean(options.sketch); } const RENDERER = Object.freeze({ id: "d2-wasm-renderer", displayName: "D2", initialize(context) { if (typeof globalThis.D2 !== "function") { throw new Error(context.t("d2RuntimeFailed")); } context.setStatusText(context.t("d2Initializing")); d2Runtime = new globalThis.D2(); context.setRendererOptions({ layout: "dagre", sketch: false }, { render: false }); }, validate(source, options, context) { if (hasUnsupportedImport(source)) { return { valid: false, error: context.t("d2ImportUnsupported") }; } if (hasUnsupportedExternalResource(source)) { return { valid: false, error: context.t("d2ExternalResource") }; } return { valid: true }; }, async render(source, options, context) { const generation = context.getRenderGeneration(); const layout = options.layout === "elk" ? "elk" : "dagre"; const sketch = Boolean(options.sketch); return queueD2Work(async () => { // A newer edit may have arrived while an older WASM job was queued. if (generation !== context.getRenderGeneration()) return null; let compiled; try { compiled = await d2Runtime.compile(source, { layout, sketch, noXMLTag: true }); } catch (error) { throw new Error(formatD2Failure(error, context, "d2CompileError")); } // Compilation can be the expensive step. Do not spend additional time // rendering a generation the framework already superseded. if (generation !== context.getRenderGeneration()) return null; let svg; try { svg = await d2Runtime.render(compiled.diagram, { ...(compiled.renderOptions || {}), sketch, noXMLTag: true }); } catch (error) { throw new Error(formatD2Failure(error, context, "d2RenderError")); } return { svg }; }); }, tidy(source) { /* The public @d2lang/d2 0.1.33 JS surface exposes compile/render but no formatter method, so this intentionally does NOT pretend to be `d2 fmt`. To avoid changing D2 semantics (especially pipe-delimited Markdown/code), Tidy only normalizes line endings, removes trailing horizontal whitespace, preserves internal blank lines, and guarantees exactly one final newline. */ const normalized = source.replace(/\r\n?/g, "\n") .split("\n") .map(line => line.replace(/[ \t]+$/g, "")) .join("\n") .replace(/\n+$/g, ""); return `${normalized}\n`; }, installControls(container, context) { const layoutLabel = document.createElement("label"); layoutLabel.className = "renderer-control"; const layoutText = document.createElement("span"); const layout = document.createElement("select"); layout.innerHTML = ''; layoutLabel.append(layoutText, layout); const sketchLabel = document.createElement("label"); sketchLabel.className = "renderer-control renderer-toggle"; const sketch = document.createElement("input"); sketch.type = "checkbox"; const sketchText = document.createElement("span"); sketchLabel.append(sketch, sketchText); container.append(layoutLabel, sketchLabel); d2ControlElements.layout = layout; d2ControlElements.sketch = sketch; d2ControlElements.layoutLabel = layoutText; d2ControlElements.sketchLabel = sketchText; layout.addEventListener("change", () => { context.setRendererOptions({ layout: layout.value }); }); sketch.addEventListener("change", () => { context.setRendererOptions({ sketch: sketch.checked }); }); syncD2Controls(context); this.applyTranslations(context); }, applyTranslations(context) { if (d2ControlElements.layoutLabel) d2ControlElements.layoutLabel.textContent = context.t("layout"); if (d2ControlElements.sketchLabel) d2ControlElements.sketchLabel.textContent = context.t("sketch"); if (d2ControlElements.layout) d2ControlElements.layout.setAttribute("aria-label", context.t("layoutAria")); if (d2ControlElements.sketch) d2ControlElements.sketch.setAttribute("aria-label", context.t("sketchAria")); }, // D2 Markdown labels need foreignObject. This hook invokes a renderer-specific, // narrowly scoped sanitizer rather than weakening the framework's generic one. sanitizeSvg(svgText) { return sanitizeD2Svg(svgText); }, installExports() { // D2 source + SVG/PNG/WebP are already framework-owned; no extra export. }, applyExampleOptions(options, context) { context.setRendererOptions({ layout: "dagre", sketch: false, ...(options || {}) }, { render: false }); syncD2Controls(context); } }); /* ============================================================================ GENERIC FRAMEWORK STATE — NORMALLY DO NOT MODIFY renderStatus has exactly three durable states: "never-rendered" no successful render has ever happened "valid" the latest completed render succeeded "invalid" the latest source failed, while lastGoodRender may remain isRendering is separate so asynchronous work does not require a fourth state. lastGoodRender is NEVER cleared merely because a later edit is blank/invalid. ============================================================================ */ const state = { renderStatus: "never-rendered", isRendering: false, lastError: "", lastGoodRender: null, rendererOptions: {}, renderGeneration: 0, renderTimer: 0, scale: 1, panX: 0, panY: 0 }; const $ = selector => document.querySelector(selector); const elements = { appTitle: $("#appTitle"), shareButton: $("#shareButton"), aboutButton: $("#aboutButton"), languageSelect: $("#languageSelect"), openButton: $("#openButton"), tidyButton: $("#tidyButton"), nameInput: $("#nameInput"), exampleSelect: $("#exampleSelect"), rendererControls: $("#rendererControls"), rendererExports: $("#rendererExports"), zoomOutButton: $("#zoomOutButton"), zoomInput: $("#zoomInput"), zoomInButton: $("#zoomInButton"), fitButton: $("#fitButton"), downloadSourceButton: $("#downloadSourceButton"), downloadSourceLabel: $("#downloadSourceLabel"), downloadSvgButton: $("#downloadSvgButton"), downloadPngButton: $("#downloadPngButton"), downloadWebpButton: $("#downloadWebpButton"), sourceSection: $("#sourceSection"), previewSection: $("#previewSection"), sourceInput: $("#sourceInput"), lineNumbers: $("#lineNumbers"), statusLine: $("#statusLine"), exportScaleWrap: $("#exportScaleWrap"), exportScaleLabel: $("#exportScaleLabel"), exportScaleInput: $("#exportScaleInput"), errorDetail: $("#errorDetail"), previewViewport: $("#previewViewport"), previewFrame: $("#previewFrame"), gestureLayer: $("#gestureLayer"), aboutDialog: $("#aboutDialog"), closeAboutButton: $("#closeAboutButton"), aboutDescription: $("#aboutDescription"), versionText: $("#versionText"), fileInput: $("#fileInput"), toast: $("#toast") }; let currentLanguage = "en"; /* ------------------------------------------------------------------------- PUBLIC CONTEXT PASSED TO RENDERER HOOKS Keep the renderer adapter dependent on this narrow API instead of arbitrary framework internals. This makes AI-assisted forks easier to audit. ------------------------------------------------------------------------- */ const rendererContext = Object.freeze({ app: APP, getSource: () => elements.sourceInput.value, getRendererOptions: () => ({ ...state.rendererOptions }), getRenderGeneration: () => state.renderGeneration, getLanguage: () => currentLanguage, t: key => t(key), setRendererOptions, requestRender: () => scheduleRender(0), setStatusText, showError, hideError, acceptRender, rejectRender, clearRender, sanitizeSvg: sanitizeRenderedSvg, downloadBlob: triggerDownload, getFilename: filenameForExtension }); function setRendererOptions(nextOptions, { render = true } = {}) { state.rendererOptions = { ...state.rendererOptions, ...nextOptions }; if (render) scheduleRender(0); } function formatMessage(template, values = {}) { return String(template).replace(/\{([a-zA-Z0-9_]+)\}/g, (_, key) => Object.prototype.hasOwnProperty.call(values, key) ? values[key] : `{${key}}` ); } function t(key) { return TRANSLATIONS[currentLanguage]?.[key] ?? TRANSLATIONS.en[key] ?? key; } function localizedOpenLabel() { return formatMessage(t("open"), { extension: `.${APP.source.extension}` }); } function applyAppMetadata() { document.title = APP.name; elements.appTitle.textContent = APP.name; elements.aboutDescription.textContent = APP.description; elements.versionText.textContent = `v${APP.version}`; elements.downloadSourceLabel.textContent = APP.source.downloadLabel; const accept = [...APP.source.acceptedExtensions, ...APP.source.acceptedMimeTypes].join(","); elements.fileInput.accept = accept; const description = document.querySelector('meta[name="description"]'); const ogTitle = document.querySelector('meta[property="og:title"]'); const ogDescription = document.querySelector('meta[property="og:description"]'); const ogUrl = document.querySelector('meta[property="og:url"]'); if (description) description.content = APP.description; if (ogTitle) ogTitle.content = APP.name; if (ogDescription) ogDescription.content = APP.description; if (ogUrl) ogUrl.content = APP.shareUrl; } /* ------------------------------------------------------------------------- INTERNATIONALIZATION ------------------------------------------------------------------------- */ const LOCALE_ALIASES = { zh: "zh-CN", "zh-Hans": "zh-CN", "zh-SG": "zh-CN", iw: "he", in: "id", no: "nb" }; function chooseLanguage() { for (const candidate of (navigator.languages || [navigator.language])) { if (!candidate) continue; const exact = LOCALE_ALIASES[candidate] || candidate; if (TRANSLATIONS[exact]) return exact; const lower = String(candidate).toLowerCase(); for (const code of Object.keys(TRANSLATIONS)) { if (code.toLowerCase() === lower) return code; } const base = LOCALE_ALIASES[lower.split("-")[0]] || lower.split("-")[0]; if (TRANSLATIONS[base]) return base; } return "en"; } function populateLanguages() { elements.languageSelect.textContent = ""; for (const code of Object.keys(TRANSLATIONS)) { const option = document.createElement("option"); option.value = code; option.textContent = LANGUAGE_META[code]?.label || code; elements.languageSelect.append(option); } currentLanguage = chooseLanguage(); elements.languageSelect.value = currentLanguage; applyTranslations(); } function applyTranslations() { document.documentElement.lang = currentLanguage; document.documentElement.dir = LANGUAGE_META[currentLanguage]?.dir || "ltr"; document.title = t("title"); elements.appTitle.textContent = t("title"); elements.aboutDescription.textContent = t("aboutDescription"); elements.openButton.textContent = localizedOpenLabel(); elements.tidyButton.textContent = t("tidy"); elements.exampleSelect.options[0].textContent = t("examples"); elements.shareButton.textContent = t("share"); elements.aboutButton.textContent = t("about"); elements.fitButton.textContent = t("fit"); elements.closeAboutButton.textContent = t("close"); $("#aboutHeading").textContent = t("about"); elements.exportScaleLabel.textContent = t("exportScale"); elements.nameInput.placeholder = t("namePlaceholder"); elements.nameInput.setAttribute("aria-label", t("namePlaceholder")); elements.languageSelect.setAttribute("aria-label", t("language")); elements.zoomOutButton.setAttribute("aria-label", t("zoomOut")); elements.zoomInButton.setAttribute("aria-label", t("zoomIn")); elements.zoomInput.setAttribute("aria-label", t("zoomPercentage")); elements.sourceSection.setAttribute("aria-label", t("sourceEditor")); elements.sourceInput.setAttribute("aria-label", t("sourceText")); elements.previewSection.setAttribute("aria-label", t("renderedPreview")); elements.previewViewport.setAttribute("aria-label", t("renderedPreview")); elements.previewFrame.title = t("previewFrameTitle"); RENDERER.applyTranslations?.(rendererContext); refreshStatusText(); } /* ------------------------------------------------------------------------- STATUS / STATE TRANSITIONS ------------------------------------------------------------------------- */ function setStatusText(message, detail = "", isError = false) { elements.statusLine.textContent = detail ? `${message} ${detail}` : message; elements.statusLine.classList.toggle("error", isError); } function showError(message) { elements.errorDetail.textContent = message; elements.errorDetail.hidden = false; } function hideError() { elements.errorDetail.textContent = ""; elements.errorDetail.hidden = true; } function refreshStatusText() { if (state.isRendering) { setStatusText(t("rendering")); return; } if (state.renderStatus === "valid" && state.lastGoodRender) { setStatusText( t("rendered"), `${Math.round(state.lastGoodRender.width)} × ${Math.round(state.lastGoodRender.height)} px` ); return; } if (state.renderStatus === "invalid") { setStatusText(t("invalid"), "", true); return; } setStatusText(t("ready")); } function updateRenderedControlAvailability() { const hasLastGood = Boolean(state.lastGoodRender); // The raw source button is never coupled to render validity. elements.downloadSourceButton.disabled = false; for (const control of [ elements.downloadSvgButton, elements.downloadPngButton, elements.downloadWebpButton, elements.zoomOutButton, elements.zoomInput, elements.zoomInButton, elements.fitButton ]) { control.disabled = !hasLastGood; } elements.exportScaleWrap.hidden = !hasLastGood; } /* Success path used by every renderer. D2 output is sanitized first, then atomically replaces lastGoodRender. An exception here leaves the previous render intact. Blank/invalid future D2 edits never clear this object, so SVG, PNG, and WebP export the most recent successfully sanitized diagram. */ function acceptRender(result) { if (!result || typeof result.svg !== "string") { throw new Error("Renderer success result must contain an SVG string."); } const sanitizedSvg = RENDERER.sanitizeSvg ? RENDERER.sanitizeSvg(result.svg, rendererContext) : sanitizeRenderedSvg(result.svg); const intrinsic = getSvgIntrinsicSize(sanitizedSvg, result.width, result.height); state.lastGoodRender = Object.freeze({ svg: sanitizedSvg, width: intrinsic.width, height: intrinsic.height, background: result.background ?? APP.output.exportBackground }); state.renderStatus = "valid"; state.isRendering = false; state.lastError = ""; renderLastGoodIntoPreview(); hideError(); updateRenderedControlAvailability(); refreshStatusText(); requestAnimationFrame(fitView); } /* Failure path deliberately does NOT clear lastGoodRender. This is the formal "invalid with last good render" behavior used by the three real apps. */ function rejectRender(error) { state.renderStatus = "invalid"; state.isRendering = false; state.lastError = error?.message || String(error || t("invalid")); showError(state.lastError); updateRenderedControlAvailability(); refreshStatusText(); } /* clearRender() is reserved for deliberate reset/startup actions. Ordinary blank or invalid edits must not call it, because they should preserve the previous successful image. */ function clearRender() { state.renderStatus = "never-rendered"; state.isRendering = false; state.lastError = ""; state.lastGoodRender = null; state.scale = 1; state.panX = 0; state.panY = 0; elements.previewFrame.srcdoc = ''; elements.previewFrame.style.width = "0px"; elements.previewFrame.style.height = "0px"; elements.previewFrame.style.transform = "none"; elements.zoomInput.value = "100%"; hideError(); updateRenderedControlAvailability(); refreshStatusText(); } /* ------------------------------------------------------------------------- SHARED SVG SECURITY BOUNDARY ------------------------------------------------------------------------- */ function parseSvgDocument(svgText) { const doc = new DOMParser().parseFromString(svgText, "image/svg+xml"); if (doc.querySelector("parsererror")) { throw new Error("Renderer produced invalid SVG markup."); } if (!doc.documentElement || doc.documentElement.localName.toLowerCase() !== "svg") { throw new Error("Renderer output is not an SVG document."); } return doc; } function sanitizeRenderedSvg(svgText) { const doc = parseSvgDocument(svgText); const root = doc.documentElement; root.querySelectorAll( "script, foreignObject, iframe, object, embed, audio, video" ).forEach(node => node.remove()); for (const element of root.querySelectorAll("*")) { for (const attribute of [...element.attributes]) { const name = attribute.name.toLowerCase(); const value = attribute.value.trim(); if (name.startsWith("on")) { element.removeAttribute(attribute.name); continue; } // External references are rejected by default. Internal #fragment URLs // are kept because many SVG renderers use them for markers/gradients. if ((name === "href" || name === "xlink:href") && value && !value.startsWith("#")) { element.removeAttribute(attribute.name); } } } // Links are unnecessary in a static image preview. Unwrap their children so // the visual output remains while navigation behavior disappears. root.querySelectorAll("a").forEach(anchor => { const group = doc.createElementNS("http://www.w3.org/2000/svg", "g"); while (anchor.firstChild) group.append(anchor.firstChild); for (const attribute of [...anchor.attributes]) { if (!/^(?:href|xlink:href)$/i.test(attribute.name)) { group.setAttribute(attribute.name, attribute.value); } } anchor.replaceWith(group); }); return new XMLSerializer().serializeToString(root); } /* D2-SPECIFIC SVG + foreignObject SECURITY PATH --------------------------------------------- D2 0.1.33 renders ordinary labels as SVG text, but rich |md Markdown labels require . Removing foreignObject would silently erase those labels, so D2 uses this narrower path instead of weakening the generic sanitizer. Defense in depth: - preview remains sandbox="" (no script privileges) - preview CSP keeps default-src/script-src/connect-src/object-src/frame-src none - script/iframe/object/embed/audio/video/forms and active HTML controls removed - all on* handlers removed - all external href/src values removed; only #fragments and data:image are kept - all links are unwrapped, so no navigation behavior remains - nested foreignObject HTML is allowlisted and its style attributes reject url(), @import, expression(), and javascript: - SVG ${safeForScriptString}`; } function renderLastGoodIntoPreview() { if (!state.lastGoodRender) return; elements.previewFrame.style.width = `${state.lastGoodRender.width}px`; elements.previewFrame.style.height = `${state.lastGoodRender.height}px`; elements.previewFrame.srcdoc = previewDocument(state.lastGoodRender.svg); } /* ------------------------------------------------------------------------- ASYNCHRONOUS RENDER PIPELINE ------------------------------------------------------------------------- */ async function renderSource() { const generation = ++state.renderGeneration; const source = elements.sourceInput.value; hideError(); if (!source.trim() && !APP.render.renderBlankSource) { // Preserve any last-good render. Blank source is not a destructive reset. state.isRendering = false; if (state.renderStatus === "never-rendered") setStatusText(t("empty")); else setStatusText(t("empty")); return; } let validation; try { validation = await RENDERER.validate(source, state.rendererOptions, rendererContext); } catch (error) { if (generation !== state.renderGeneration) return; rejectRender(error); return; } if (generation !== state.renderGeneration) return; if (validation && validation.valid === false) { rejectRender(new Error(validation.error || t("invalid"))); return; } state.isRendering = true; refreshStatusText(); try { const result = await RENDERER.render(source, state.rendererOptions, rendererContext); if (generation !== state.renderGeneration) return; if (result == null) { state.isRendering = false; if (state.renderStatus === "never-rendered") { setStatusText(t("noRenderer")); } else { refreshStatusText(); } return; } acceptRender(result); } catch (error) { if (generation !== state.renderGeneration) return; rejectRender(error); } } function scheduleRender(delay = APP.render.debounceMs) { clearTimeout(state.renderTimer); state.renderTimer = setTimeout(renderSource, delay); } /* ------------------------------------------------------------------------- TIDY — DISPATCHED TO THE RENDERER ------------------------------------------------------------------------- */ async function tidySource() { try { const tidied = await RENDERER.tidy( elements.sourceInput.value, state.rendererOptions, rendererContext ); elements.sourceInput.value = String(tidied ?? elements.sourceInput.value); updateLineNumbers(); scheduleRender(0); } catch (error) { rejectRender(error); } } /* ------------------------------------------------------------------------- EDITOR / FILE HELPERS ------------------------------------------------------------------------- */ function updateLineNumbers() { const count = Math.max(1, elements.sourceInput.value.split("\n").length); let output = ""; for (let line = 1; line <= count; line++) { output += line; if (line < count) output += "\n"; } elements.lineNumbers.textContent = output; } function sanitizeFilename(name) { const cleaned = String(name || "") .replace(/[\\/:*?"<>|]/g, "") .replace(/[\u0000-\u001f\u007f]/g, "") .replace(/\s+/g, " ") .trim(); return cleaned || APP.defaultFilename; } function filenameForExtension(extension) { return `${sanitizeFilename(elements.nameInput.value)}.${String(extension).replace(/^\./, "")}`; } async function openFile(file) { if (!file) return; try { elements.sourceInput.value = (await file.text()).replace(/\r\n?/g, "\n"); updateLineNumbers(); if (!elements.nameInput.value.trim()) { elements.nameInput.value = sanitizeFilename(file.name.replace(/\.[^.]+$/, "")); } scheduleRender(0); } catch (error) { setStatusText(t("openFailed"), "", true); showError(error?.message || String(error)); } } function populateExamples() { for (const example of EXAMPLES) { const option = document.createElement("option"); option.value = example.id; option.textContent = example.label; elements.exampleSelect.append(option); } } async function loadExample(example) { if (!example) return; const source = example.source.endsWith("\n") ? example.source : `${example.source}\n`; elements.sourceInput.value = source; elements.exampleSelect.value = ""; if (!elements.nameInput.value.trim()) { elements.nameInput.value = sanitizeFilename( example.label.toLowerCase().replace(/\s+/g, "-") ); } await RENDERER.applyExampleOptions?.( { ...(example.rendererOptions || {}) }, rendererContext ); updateLineNumbers(); scheduleRender(0); elements.sourceInput.focus(); } /* ------------------------------------------------------------------------- PAN / ZOOM / FIT — GENERIC FOR ALL SVG-BACKED RENDERERS ------------------------------------------------------------------------- */ function clampZoom(value) { return Math.min(32, Math.max(0.02, value)); } function applyViewTransform() { elements.previewFrame.style.transform = `translate(${state.panX}px, ${state.panY}px) scale(${state.scale})`; elements.zoomInput.value = `${Math.round(state.scale * 100)}%`; } function fitView() { if (!state.lastGoodRender) return; const rect = elements.previewViewport.getBoundingClientRect(); const padding = Math.min(36, Math.max(14, Math.min(rect.width, rect.height) * 0.05)); const availableWidth = Math.max(1, rect.width - padding * 2); const availableHeight = Math.max(1, rect.height - padding * 2); state.scale = clampZoom(Math.min( availableWidth / state.lastGoodRender.width, availableHeight / state.lastGoodRender.height )); state.panX = (rect.width - state.lastGoodRender.width * state.scale) / 2; state.panY = (rect.height - state.lastGoodRender.height * state.scale) / 2; applyViewTransform(); } function zoomAt(nextScale, clientX, clientY) { if (!state.lastGoodRender) return; nextScale = clampZoom(nextScale); const rect = elements.previewViewport.getBoundingClientRect(); const x = clientX - rect.left; const y = clientY - rect.top; const imageX = (x - state.panX) / state.scale; const imageY = (y - state.panY) / state.scale; state.scale = nextScale; state.panX = x - imageX * state.scale; state.panY = y - imageY * state.scale; applyViewTransform(); } function zoomAtCenter(nextScale) { const rect = elements.previewViewport.getBoundingClientRect(); zoomAt(nextScale, rect.left + rect.width / 2, rect.top + rect.height / 2); } function commitZoomInput() { const parsed = Number.parseFloat(elements.zoomInput.value.replace("%", "").trim()); if (!Number.isFinite(parsed) || parsed <= 0) { applyViewTransform(); return; } zoomAtCenter(parsed / 100); } /* ------------------------------------------------------------------------- EXPORT SCALE / BITMAP DIMENSIONS ------------------------------------------------------------------------- */ function exportScaleFactor() { const parsed = Number.parseFloat(elements.exportScaleInput.value.replace("%", "").trim()); const fallback = APP.bitmap.defaultExportScalePercent; const percent = Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; const clamped = Math.min( APP.bitmap.maxExportScalePercent, Math.max(APP.bitmap.minExportScalePercent, percent) ); return clamped / 100; } function commitExportScale() { elements.exportScaleInput.value = `${Math.round(exportScaleFactor() * 100)}%`; } function calculateBitmapDimensions(render, scaleFactor = exportScaleFactor()) { const density = APP.bitmap.basePixelDensity; const requestedWidth = Math.max(1, Math.round(render.width * density * scaleFactor)); const requestedHeight = Math.max(1, Math.round(render.height * density * scaleFactor)); const largest = Math.max(requestedWidth, requestedHeight); const capRatio = Math.min(1, APP.bitmap.maxDimension / largest); return { width: Math.max(1, Math.round(requestedWidth * capRatio)), height: Math.max(1, Math.round(requestedHeight * capRatio)) }; } /* ------------------------------------------------------------------------- EXPORTS — SOURCE IS ALWAYS INDEPENDENT FROM RENDER VALIDITY D2 downloads use the editor value byte-for-byte as JavaScript sees it, even when blank or invalid. Rendered formats use only lastGoodRender. Bitmap output uses the framework's 2× base density multiplied by Export Scale. ------------------------------------------------------------------------- */ function triggerDownload(blob, filename) { const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = filename; document.body.append(anchor); anchor.click(); anchor.remove(); setTimeout(() => URL.revokeObjectURL(url), 1500); } function downloadSource() { // This must always mirror the editor text exactly, even blank or invalid. triggerDownload( new Blob([elements.sourceInput.value], { type: APP.source.defaultMimeType }), filenameForExtension(APP.source.extension) ); } function downloadRenderedSvg() { if (!state.lastGoodRender) return; triggerDownload( new Blob([state.lastGoodRender.svg], { type: "image/svg+xml;charset=utf-8" }), filenameForExtension("svg") ); } async function renderedSvgToRaster(mimeType) { if (!state.lastGoodRender) return null; const dimensions = calculateBitmapDimensions(state.lastGoodRender); const sourceUrl = URL.createObjectURL( new Blob([state.lastGoodRender.svg], { type: "image/svg+xml;charset=utf-8" }) ); try { const image = new Image(); await new Promise((resolve, reject) => { image.onload = resolve; image.onerror = () => reject(new Error("Could not rasterize rendered SVG.")); image.src = sourceUrl; }); const canvas = document.createElement("canvas"); canvas.width = dimensions.width; canvas.height = dimensions.height; const context = canvas.getContext("2d"); context.imageSmoothingEnabled = true; context.imageSmoothingQuality = "high"; const background = state.lastGoodRender.background ?? APP.output.exportBackground; if (!APP.output.preserveTransparency || (background && background !== "transparent")) { context.save(); context.fillStyle = background === "transparent" ? "#ffffff" : background; context.fillRect(0, 0, canvas.width, canvas.height); context.restore(); } else { context.clearRect(0, 0, canvas.width, canvas.height); } context.drawImage(image, 0, 0, canvas.width, canvas.height); const blob = await new Promise(resolve => canvas.toBlob(resolve, mimeType)); if (!blob) throw new Error(t("exportFailed")); return blob; } finally { URL.revokeObjectURL(sourceUrl); } } async function downloadRenderedRaster(mimeType, extension) { if (!state.lastGoodRender) return; try { const blob = await renderedSvgToRaster(mimeType); triggerDownload(blob, filenameForExtension(extension)); } catch (error) { setStatusText(t("exportFailed"), "", true); showError(error?.message || String(error)); } } /* ------------------------------------------------------------------------- SHARE / TOAST ------------------------------------------------------------------------- */ async function copyShareText() { const text = `${t("title")} — ${t("aboutDescription")}`; try { await navigator.clipboard.writeText(text); } catch { const textarea = document.createElement("textarea"); textarea.value = text; document.body.append(textarea); textarea.select(); document.execCommand("copy"); textarea.remove(); } showToast(t("copied")); } function showToast(message) { elements.toast.textContent = message; elements.toast.classList.add("show"); clearTimeout(showToast.timer); showToast.timer = setTimeout(() => elements.toast.classList.remove("show"), 1500); } /* ------------------------------------------------------------------------- EVENT WIRING ------------------------------------------------------------------------- */ function installEvents() { elements.sourceInput.addEventListener("input", () => { updateLineNumbers(); scheduleRender(); }); elements.sourceInput.addEventListener("scroll", () => { elements.lineNumbers.scrollTop = elements.sourceInput.scrollTop; }); elements.sourceInput.addEventListener("keydown", event => { if (event.key !== "Tab") return; event.preventDefault(); elements.sourceInput.setRangeText( " ", elements.sourceInput.selectionStart, elements.sourceInput.selectionEnd, "end" ); updateLineNumbers(); scheduleRender(); }); elements.openButton.addEventListener("click", () => elements.fileInput.click()); elements.fileInput.addEventListener("change", event => { openFile(event.target.files?.[0]); event.target.value = ""; }); elements.tidyButton.addEventListener("click", tidySource); elements.exampleSelect.addEventListener("change", () => { loadExample(EXAMPLES.find(example => example.id === elements.exampleSelect.value)); }); elements.languageSelect.addEventListener("change", () => { currentLanguage = elements.languageSelect.value; applyTranslations(); }); elements.shareButton.addEventListener("click", copyShareText); elements.aboutButton.addEventListener("click", () => elements.aboutDialog.showModal()); elements.closeAboutButton.addEventListener("click", () => elements.aboutDialog.close()); elements.aboutDialog.addEventListener("click", event => { if (event.target === elements.aboutDialog) elements.aboutDialog.close(); }); elements.downloadSourceButton.addEventListener("click", downloadSource); elements.downloadSvgButton.addEventListener("click", downloadRenderedSvg); elements.downloadPngButton.addEventListener("click", () => downloadRenderedRaster("image/png", "png")); elements.downloadWebpButton.addEventListener("click", () => downloadRenderedRaster("image/webp", "webp")); elements.zoomOutButton.addEventListener("click", () => zoomAtCenter(state.scale / 1.25)); elements.zoomInButton.addEventListener("click", () => zoomAtCenter(state.scale * 1.25)); elements.fitButton.addEventListener("click", fitView); elements.zoomInput.addEventListener("change", commitZoomInput); elements.zoomInput.addEventListener("keydown", event => { if (event.key === "Enter") { event.preventDefault(); commitZoomInput(); elements.zoomInput.select(); } }); elements.exportScaleInput.addEventListener("change", commitExportScale); elements.exportScaleInput.addEventListener("blur", commitExportScale); elements.exportScaleInput.addEventListener("keydown", event => { if (event.key === "Enter") { event.preventDefault(); commitExportScale(); elements.exportScaleInput.select(); } }); window.addEventListener("dragover", event => event.preventDefault()); window.addEventListener("drop", event => { event.preventDefault(); openFile(event.dataTransfer?.files?.[0]); }); elements.gestureLayer.addEventListener("wheel", event => { if (!state.lastGoodRender) return; event.preventDefault(); zoomAt( state.scale * Math.exp(-event.deltaY * 0.0015), event.clientX, event.clientY ); }, { passive: false }); let dragging = false; let pointerId = null; let lastX = 0; let lastY = 0; elements.gestureLayer.addEventListener("pointerdown", event => { if (!state.lastGoodRender || event.button !== 0) return; dragging = true; pointerId = event.pointerId; lastX = event.clientX; lastY = event.clientY; elements.gestureLayer.setPointerCapture(event.pointerId); elements.gestureLayer.style.cursor = "grabbing"; }); elements.gestureLayer.addEventListener("pointermove", event => { if (!dragging || event.pointerId !== pointerId) return; state.panX += event.clientX - lastX; state.panY += event.clientY - lastY; lastX = event.clientX; lastY = event.clientY; applyViewTransform(); }); const endDrag = event => { if (event.pointerId !== pointerId) return; dragging = false; pointerId = null; elements.gestureLayer.style.cursor = "grab"; }; elements.gestureLayer.addEventListener("pointerup", endDrag); elements.gestureLayer.addEventListener("pointercancel", endDrag); elements.previewViewport.addEventListener("keydown", event => { if (!state.lastGoodRender || !["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) return; event.preventDefault(); const amount = event.shiftKey ? 60 : 20; if (event.key === "ArrowLeft") state.panX -= amount; if (event.key === "ArrowRight") state.panX += amount; if (event.key === "ArrowUp") state.panY -= amount; if (event.key === "ArrowDown") state.panY += amount; applyViewTransform(); }); new ResizeObserver(() => { if (state.lastGoodRender) fitView(); }).observe(elements.previewViewport); } /* ------------------------------------------------------------------------- LIGHTWEIGHT DEVELOPMENT SELF-TESTS No external test framework is required. These assertions verify invariants that are easy to accidentally break while adapting the template. ------------------------------------------------------------------------- */ function runSelfTests() { const failures = []; const check = (condition, message) => { console.assert(condition, message); if (!condition) failures.push(message); }; check(elements.downloadSourceButton.disabled === false, "Raw source download must always be enabled."); check(elements.previewFrame.hasAttribute("sandbox"), "Preview iframe must remain sandboxed."); check(EXAMPLES.every(example => example.source.endsWith("\n")), "Every example must end with a newline."); check(Object.keys(TRANSLATIONS).length === Object.keys(LANGUAGE_META).length, "Translation and language metadata counts must match."); check(Object.keys(TRANSLATIONS).every(code => LANGUAGE_META[code]), "Every translation must have language metadata."); const translationKeys = Object.keys(TRANSLATIONS.en).sort().join("|"); check(Object.values(TRANSLATIONS).every(strings => Object.keys(strings).sort().join("|") === translationKeys), "Every translation must have exact key parity with English."); check(APP.source.extension === "d2" && APP.source.acceptedExtensions.includes(".txt"), "D2 and TXT source types must be configured."); check(APP.development.showTemplateTestImage === false && APP.render.renderBlankSource === false, "D2 app must start blank without the template test image."); check(![...elements.rendererControls.querySelectorAll("option")].some(option => /tala/i.test(option.value + option.textContent)), "TALA must not be offered by the browser/WASM renderer."); check(sanitizeFilename('a:c?.txt') === 'abc.txt', "Filename sanitization should remove invalid filesystem characters."); const testRender = { width: 600, height: 400 }; const at100 = calculateBitmapDimensions(testRender, 1); const at200 = calculateBitmapDimensions(testRender, 2); check(at100.width === 1200 && at100.height === 800, "100% bitmap export should honor the configured 2× base pixel density."); check(at200.width === 2400 && at200.height === 1600, "200% bitmap export should double the 100% bitmap dimensions."); const malicious = 'alert(1)'; const sanitized = sanitizeRenderedSvg(malicious); check(!/