Skip to content

MDX Messages

Palamedes treats .mdx as a first-class source format. The native extractor and the Vite compiler consume the same FerroMark semantic event stream, so a message extracted by pmds extract is the same message rendered by the compiled module.

Page outline

Setup

React MDX compilation requires Vite 8 or newer. Its Rolldown pipeline accepts the generated JSX module type that lets Palamedes compile .mdx before React processes it. Install the framework package and use the Palamedes plugin before the framework Vite plugin:

import react from "@vitejs/plugin-react"
import { palamedes } from "@palamedes/vite-plugin"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [palamedes(), react()],
})

For React, Palamedes marks generated .mdx modules as JSX automatically. Vite 7 uses Rollup rather than Rolldown, so it cannot consume this React MDX output; upgrade to Vite 8 or set mdx: false while keeping macro and catalog support.

For Solid, set framework: "solid" on the plugin and opt .mdx into vite-plugin-solid explicitly:

import solid from "vite-plugin-solid"
import { palamedes } from "@palamedes/vite-plugin"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [palamedes({ framework: "solid" }), solid({ extensions: [".mdx"] })],
})

The plugin compiles for React unless told otherwise, so this option is required for Solid. Framework selection controls only the rich-component contract; runtime lookups stay hook-free. Extraction produces identical messages for both frameworks, so the catalog config has no reason to know the application runtime.

Do not add React's JSX module type for Solid. Rolldown would otherwise lower the module with React's automatic runtime before Solid's Babel preset sees it. Projects that must remain on Vite 7 or older can continue using the macro and catalog plugins with palamedes({ mdx: false }).

Palamedes and @mdx-js/rollup both claim .mdx modules. Do not enable both for the same files; Palamedes runs as a pre-transform and intentionally owns the full MDX compilation path.

Run normal catalog extraction after adding an MDX file:

pmds extract

Directory include patterns discover .mdx alongside JavaScript and TypeScript.

Translation Units

Palamedes derives messages from rendered semantics rather than raw Markdown tokens:

  • each heading, paragraph, list item, blockquote paragraph, and table cell is its own message
  • inline emphasis, links, and JSX become rich placeholders
  • MDX expressions become named value placeholders
  • image alt text is extracted separately
  • configured static JSX attributes and frontmatter scalar fields are extracted separately

For example:

# Hello {account.name}

Read the **guide** or visit [configuration](/docs/configuration).

extracts messages equivalent to:

Hello {name}
Read the <0>guide</0> or visit <1>configuration</1>.

The generated module preserves account.name as the runtime expression behind the name placeholder. Repeated expressions reuse the same placeholder. URLs, image sources, fenced code blocks, and inline code are rendered but are not sent to translators. Fenced code keeps a language-* class derived from its info string.

Configuration

mdx:
  framework: react
  translatable-attributes: [alt, title, aria-label]
  front-matter-fields: [title, description]
  trans-module: "@palamedes/react/compiled"
  runtime-module: "@palamedes/runtime"
  ignore-directive: palamedes-ignore
FieldDefaultPurpose
frameworkreactGenerates React or Solid rich-component bindings.
translatable-attributes[alt]Static JSX attributes extracted as standalone messages.
front-matter-fields[]Scalar frontmatter values extracted as standalone messages.
trans-moduleframework /compiled pathModule exporting parser-free Trans.
runtime-module@palamedes/runtimeAdvanced override for the module exporting getI18n.
ignore-directivepalamedes-ignoreMarker used for an explicit per-unit opt-out.

TypeScript configs use the camelCase equivalents: translatableAttributes, frontMatterFields, transModule, runtimeModule, and ignoreDirective.

translatable-attributes replaces the default list rather than extending it. Include alt explicitly when adding fields such as title or aria-label. href and src remain structural attributes and are never extracted. In Vite, the plugin's framework option selects the component contract for compiled MDX; mdx.framework overrides it per config. Runtime access remains hook-free. The plugin's runtimeModule option applies to macros only — use mdx.runtime-module to point MDX at a different custom runtime explicitly.

The Vite plugin preserves inline source-message fallbacks in both development and production builds. This makes a missing production catalog chunk readable during deploy skew. Configure keepSourceFallbacks: false on palamedes() to opt into the smaller, hash-only output when source text cannot be shipped. MdxOptions accepts the same key in data config, but the Vite plugin overwrites it with its own build-output setting, so configure the plugin option when using Vite.

The Vite plugin's top-level framework: "none" option drops the plugin's own MDX framework selection; it does not disable MDX compilation. The compiler then falls back to its default and still emits the React component contract. A project that wants no MDX component contract at all sets mdx: false. The data-config override mdx.framework selects between "react" and "solid" and accepts no other value; @palamedes/remix does not expose the Vite plugin option.

Translated frontmatter remains explicit. The compiler exports the original scalar object as frontmatter; when configured fields are present it also exports getTranslatedFrontmatter(), which resolves those fields through the active i18n instance. Collection-valued frontmatter is omitted rather than being stringified as a misleading scalar.

Ignoring Content

Put the configured marker in a comment immediately before a semantic unit:

{/* palamedes-ignore */}

This paragraph stays in the source language.

The content still renders, but no catalog message is created for that unit. The directive covers the whole unit, including image alt text and configured JSX attributes. JSX comments are the canonical MDX form; HTML comments are also recognized and are omitted from rendered content.

Diagnostics, Maps, and HMR

Malformed MDX produces structured, source-ranged diagnostics from FerroMark. Footnotes currently produce an UnsupportedFootnote diagnostic instead of silently compiling incorrect links. The Vite adapter reports diagnostic locations as build errors and returns the native v3 source map for valid modules. It watches the Palamedes config; catalog modules remain owned by the normal PO loader and configured runtime.

Generated modules currently use the built-in HTML element mapping. They do not yet implement MDXProvider or a components prop, and extracted messages do not set context. Use imported JSX components directly when a page needs custom rendering.

Verified Example

examples/vite-mdx is a complete three-page React/Vite handbook with linked MDX modules, a shared English/German catalog, imported JSX components, expressions, translated attributes, image alt text, code blocks, and a document-level locale switch. It is part of pnpm verify:examples.

Architecture Boundary

FerroMark owns MDX recognition and the versioned semantic event stream. crates/palamedes owns translation-unit selection, rich/value placeholder construction, message identity, diagnostics conversion, and JSX module lowering. The native CLI and typed N-API binding both call that core workflow. @palamedes/vite-plugin is intentionally a thin host adapter: it selects .mdx modules, forwards configuration, registers watch files, and returns the native code and source map.

This boundary prevents extraction and compilation from developing separate Markdown heuristics.