@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.

  1. tools/emailemail-safe HTML and plain text
  2. tools/printblueprint to document to PDF
  3. player print modethe static form the browser prints
  4. core/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 three drafts, rendered at build time
DraftOutcomeDiagnostics
Weekly digestEmail of 8,822 characters
  • warning email-unsupported: The `chart` block has no email form; its text stays.
With a javascript: linkRefused: nothing written
  • error sink: an executable string
  • error sink: a URL the untrusted URL policy refuses
  • error block-schema: block actions.0.href: href must be an https, mailto or same-document link (untrusted tier)
With raw markup and handlersRefused: nothing written
  • error sink: a forbidden "style" key
  • error sink: an event handler key
  • error sink: an HTML string
  • error forbidden-prop: Prop "style" is never passed through.
  • error invalid-prop: Unknown prop "onclick" was dropped.
  • error block-schema: block markdown: Expected a string

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.

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.

blueprintToDocument: 11 document blocks
Block typeCount
heading2
paragraph4
list1
block2
callout1
quote1

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.

Locale

English is active.

Rating
3 of 5
  • launch
  • billing
npm install texuli
1 file

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.

313 English labels, by component
NamespaceLabels
texuli.alert-dialog3
texuli.alert-surface1
texuli.app-nav4
texuli.app-shell4
texuli.auth-split-layout2
texuli.bottom-nav1
texuli.breadcrumb2
texuli.calendar2
texuli.carousel10
texuli.collapsible1
texuli.color-picker2
texuli.combobox-empty1
texuli.combobox-error2
texuli.combobox-loading1
texuli.command-input2
texuli.command-palette9
texuli.command-root1
texuli.command-trigger1
texuli.content-layout4
texuli.copy-button2
texuli.dashboard-grid-layout2
texuli.date-range-picker7
texuli.dialog1
texuli.ds-kit126
texuli.dual-sidebar-layout2
texuli.error-state2
texuli.file-upload10
texuli.input-group1
texuli.input1
texuli.kpi1
texuli.landing-layout1
texuli.layout-chrome1
texuli.layout-host1
texuli.marketing-layout3
texuli.master-detail-layout4
texuli.mobile-app-frame1
texuli.mobile-header1
texuli.mobile-layout7
texuli.pricing-layout2
texuli.progress1
texuli.rail-layout1
texuli.rating5
texuli.resizable-handle1
texuli.settings-layout1
texuli.sheet-header1
texuli.sidebar-layout1
texuli.sidebar3
texuli.slider1
texuli.split-view2
texuli.stat3
texuli.stepper4
texuli.tag1
texuli.tags-input2
texuli.timeline4
texuli.toaster2
texuli.top-nav-layout1
texuli.tree-view1
texuli.wizard-layout1
texuli.wizard5
primitives.calendar2
primitives.color-picker1
primitives.combobox4
primitives.command2
primitives.date-field5
primitives.date-picker9
primitives.file-drop2
primitives.number-field2
primitives.pagination6
primitives.tags-input6
primitives.toast2
primitives.tree2

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 id
  • texuli.copy-button.label: the message is too long
  • texuli.tag.remove-label: a plural or select message needs an `other` string
  • texuli.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 service

Package 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