@texuli/tools/email · @texuli/tools/print · @texuli/core/i18n · new in 0.8.0
One blueprint: a page, an email, a PDF, in any language
The same validated blueprint that paints in the player becomes an email that mail clients render, a print-ready page and a PDF, and every label the components show comes from one message catalog. These targets are on npm from 0.8.0, a pre-1.0 release; 0.7.0 and earlier are without them.
tools/emailemail-safe HTML and plain texttools/printblueprint to document to PDFplayer print modethe static form the browser printscore/i18nthe message catalog, lazy locales
Email from a blueprint
renderEmail validates a blueprint at the untrusted tier, then writes a complete email: tables and inline styles mail clients understand, every value escaped, links and images through the untrusted URL policy, and a plain-text part. Content with no email form falls back to its text with a warning; a draft with anything executable is refused whole.
The weekly digest as an email
Written by renderEmail when this page was built, served as its own file and shown on request in a frame with every sandbox restriction on.
Render it yourself
The renderer and the catalog it validates against load when you ask.
| Draft | Outcome | Diagnostics |
|---|---|---|
| Weekly digest | Email of 8,822 characters |
|
| With a javascript: link | Refused: nothing written |
|
| With raw markup and handlers | Refused: nothing written |
|
Ids that paint in email (16)
layout-page, layout-doc, layout-card, layout-grid, layout-deck, deck-slide, layout-stack, layout-row, layout-split, layout-tile, blocks-text, blocks-structure, blocks-data, blocks-media, blocks-code, media-image
Block types inside them (16)
heading, text, list, quote, callout, divider, code, cta, columns, cards, image, figure, gallery, table, stat, kpi
Anything else (charts, diagrams, maps, interactive blocks) falls back to its text. Planned: a dark-mode rule set in the head of the email (today it declares color-scheme: light) and an email envelope profile in a later spec minor.
Print and PDF
Two ways to print the same blueprint. In the browser the player has a print mode, a static paint with page-break rules for its frames, tables and code; the print dialog prints this whole page with the digest on it. Headless, @texuli/tools/print maps the blueprint to a validated document and the owned PDF writer draws it, with no JavaScript, forms or embedded files in the file.
Loading the player…
| Block type | Count |
|---|---|
heading | 2 |
paragraph | 4 |
list | 1 |
block | 2 |
callout | 1 |
quote | 1 |
Diagnostics of the mapping: The `chart` block has no document form; its text stays.
import { renderBlueprintPdf } from '@texuli/tools/print';
const pdf = await renderBlueprintPdf(agentDraft, {
page: { size: 'a4', orientation: 'portrait' },
});
if (pdf.ok) downloadBlob(new Blob([pdf.bytes], { type: pdf.mime }), 'digest.pdf');
<Player blueprint={agentDraft} print />Every label from one message catalog
Component labels resolve through @texuli/core/i18n: an instance prop first, then the catalog in context, then the English default next to the component. Locales load lazily, plurals and selects follow Intl, and catalogs from outside are validated. English is the default, and opt-in Spanish catalogs ship from 0.14.3; other translations are planned, and hosts can load their own catalogs today.
English is active.
npm install texuli The tag group’s name, “Topics”, is an instance prop, so it stays as written in every locale; the rest come from the catalog. Catalogs loaded: en.
| Namespace | Labels |
|---|---|
texuli.alert-dialog | 3 |
texuli.alert-surface | 1 |
texuli.app-nav | 4 |
texuli.app-shell | 4 |
texuli.auth-split-layout | 2 |
texuli.bottom-nav | 1 |
texuli.breadcrumb | 2 |
texuli.calendar | 2 |
texuli.carousel | 10 |
texuli.collapsible | 1 |
texuli.color-picker | 2 |
texuli.combobox-empty | 1 |
texuli.combobox-error | 2 |
texuli.combobox-loading | 1 |
texuli.command-input | 2 |
texuli.command-palette | 9 |
texuli.command-root | 1 |
texuli.command-trigger | 1 |
texuli.content-layout | 4 |
texuli.copy-button | 2 |
texuli.dashboard-grid-layout | 2 |
texuli.date-range-picker | 7 |
texuli.dialog | 1 |
texuli.ds-kit | 126 |
texuli.dual-sidebar-layout | 2 |
texuli.error-state | 2 |
texuli.file-upload | 10 |
texuli.input-group | 1 |
texuli.input | 1 |
texuli.kpi | 1 |
texuli.landing-layout | 1 |
texuli.layout-chrome | 1 |
texuli.layout-host | 1 |
texuli.marketing-layout | 3 |
texuli.master-detail-layout | 4 |
texuli.mobile-app-frame | 1 |
texuli.mobile-header | 1 |
texuli.mobile-layout | 7 |
texuli.pricing-layout | 2 |
texuli.progress | 1 |
texuli.rail-layout | 1 |
texuli.rating | 5 |
texuli.resizable-handle | 1 |
texuli.settings-layout | 1 |
texuli.sheet-header | 1 |
texuli.sidebar-layout | 1 |
texuli.sidebar | 3 |
texuli.slider | 1 |
texuli.split-view | 2 |
texuli.stat | 3 |
texuli.stepper | 4 |
texuli.tag | 1 |
texuli.tags-input | 2 |
texuli.timeline | 4 |
texuli.toaster | 2 |
texuli.top-nav-layout | 1 |
texuli.tree-view | 1 |
texuli.wizard-layout | 1 |
texuli.wizard | 5 |
primitives.calendar | 2 |
primitives.color-picker | 1 |
primitives.combobox | 4 |
primitives.command | 2 |
primitives.date-field | 5 |
primitives.date-picker | 9 |
primitives.file-drop | 2 |
primitives.number-field | 2 |
primitives.pagination | 6 |
primitives.tags-input | 6 |
primitives.toast | 2 |
primitives.tree | 2 |
A translation file from outside
parseCatalog kept 2 messages and dropped 5:
not an id: not a namespaced message id__proto__: not a namespaced message idtexuli.copy-button.label: the message is too longtexuli.tag.remove-label: a plural or select message needs an `other` stringtexuli.toaster.label: a message is a string, or a plural or select object
The pseudo-locale
- Close → ⟦Çlósé⟧
- Next page → ⟦Ñéxt págé⟧
- Remove {label} → ⟦Rémóvé {label}⟧
import { createI18n } from '@texuli/core/i18n';
import { setI18nContext } from 'texuli';
const i18n = createI18n({
locale: 'en',
loaders: { 'pt-BR': () => import('./locales/pt-BR.ts') }, // loads only when active
});
setI18nContext(i18n); // in the root component's script
await i18n.setLocale('pt-BR');What keeps it safe
Static outputs are built, not templated. These hold by construction.
Validation first
Email and print run validateDraft at the untrusted tier with the deep block schemas and the sink scan before mapping anything; only the sink-free render tree is ever read.
Escaped by construction
Email HTML is built with the markup builders of @texuli/core: element and attribute names are allowlisted, every value is escaped, and there is no raw-markup API. No scripts, style elements, forms, frames or event handlers.
Links and images through the URL policy
Links must end up absolute https or mailto, images absolute https, at the untrusted tier. A refused link keeps its text; a refused image keeps its alt text.
Messages are text
formatMessage makes one pass with no recursion: a value is never scanned again, so a value holding {name} stays literal. Translation files are checked by parseCatalog: bad ids, prototype keys and oversized messages are dropped with an issue.
import { renderEmail } from '@texuli/tools/email';
const { ok, html, text, diagnostics } = renderEmail(agentDraft, {
preheader: 'Orders up 12%',
baseUrl: 'https://example.com/',
});
if (ok) await mailer.send({ html, text }); // your mail servicePackage docs
The site does not render the package docs yet. Read them in the repository:
- Email:
packages/tools/docs/email.md - Print and PDF:
packages/tools/docs/print.md - Player print mode:
packages/player/docs/embedding.md - Message catalog:
packages/core/docs/core-i18n.md - Primitive labels:
packages/primitives/docs/i18n.md

