ResilientLang docsDeveloper documentation (draft, product name is a placeholder)

Framework and static-site SDKs

Status. These four packages exist in the repository (packages/next, packages/nuxt, packages/astro, packages/static-prerender) and are tested offline with sample sites. They are private: nothing is published to npm. Not tested against a real search-engine crawl.

@resilientlang/static-prerender is the engine; the Next, Nuxt and Astro packages wrap it. They solve the search problem of the script snippet: at build time they take your built pages and a published, signed bundle and write a translated copy of every page under /<prefix>/ (for example /fr/about) with <html lang dir>, reciprocal hreflang (plus x-default), a canonical link, the loader tag and, optionally, a sitemap with alternates. Visitors and crawlers get translated HTML from your own host with no request to us, so it keeps working when our servers are down.

Extraction and apply follow the extraction rules exactly; the package runs all 46 extraction vectors and 11 apply cases.

If you cannot prerender at build time (WordPress, server-rendered shops, a site behind a CDN with edge code), see Edge delivery, which applies the same engine at request time.

Where the bundle comes from#

bundle: { dir } (a folder with manifest.json and <version>/<lang>.<hash8>.json, as produced by rl pull) or { bases: [https origins in order], siteKey }. The manifest signature is mandatory, each language file is checked against the manifest SHA-256, minVersion blocks rollbacks, and kid=key entries work as in the loader. cacheDir keeps the last verified bundle so a build still works when every origin is down. If nothing can be loaded, the default (onError: 'warn') leaves the build output untouched (source language only). A bad signature, a rollback or a hash mismatch always fails the build. The allowUnsigned option exists for local development only; never use it for a real build.

Next.js#

Next.js writes plain HTML only with output: 'export', and its docs describe no after-export hook (an absence in the docs, not a statement that none exists), which is why rl-next runs after next build. With a static export the CSP header must come from your host: Next.js nonces need dynamic rendering.

// next.config.js
const { withResilientLang } = require('@resilientlang/next');
module.exports = withResilientLang({ output: 'export' });
{ "scripts": { "build": "next build && rl-next" } }

resilientlang.config.json holds public values only (a secret rl_sec_ key is refused): { "sourceLanguage": "en", "siteUrl": "https://example.com", "bundle": { "dir": "./rl-bundle" }, "publicKeys": ["<key>"] }. Run rl-next --dir out if you set a different distDir.

Nuxt#

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@resilientlang/nuxt'],
  resilientLang: { sourceLanguage: 'en', siteUrl: 'https://example.com', bundle: { dir: './rl-bundle' }, publicKeys: ['<key>'] },
})

Run nuxt generate. The module relies on Nitro's prerender:done hook, which Nuxt's hook documentation does not list (verify); the package's author checked it with a real Nuxt build. A nuxt build with the node-server preset has no static pages and is not touched; development mode is skipped.

Astro#

// astro.config.mjs
import resilientLang from '@resilientlang/astro';
export default defineConfig({ site: 'https://example.com', integrations: [resilientLang({
  sourceLanguage: 'en', bundle: { dir: './rl-bundle' }, publicKeys: ['<base64url Ed25519 public key>'],
  loader: { siteKey: 'rl_site_live_...', loaderUrl: 'https://cdn.example/loader/v1/loader.js', publicKey: '<same key>' },
})] });

It runs after the Astro build. Astro's site becomes siteUrl.

Plain static HTML (@resilientlang/static-prerender)#

node dist/cli.js run --in dist --source-language en --site-url https://example.com \
  --bundle-dir ./rl-bundle --public-key <base64url> --loader-url https://cdn.example/loader/v1/loader.js --site-key rl_site_live_...
node dist/cli.js extract --in dist/index.html     # the unique translatable strings as JSON

For server-rendered or edge handlers you write yourself, translateHtml(bundle, html, lang) translates one HTML response.

Options that matter#

OptionDefaultMeaning
siteUrlnoneNeeded for hreflang, canonical and the sitemap (Google wants fully qualified hreflang URLs)
hydrationssrssr marks <html data-rl-ssr> so the loader leaves the page alone (right for Astro and plain HTML). strip gives translated copies no framework scripts, so nothing can hydrate over the translation (right for Next and Nuxt)
cleanUrlstrueabout.html is public as /about; false keeps /about.html
rewriteLinkstrueRoot-relative links to pages that exist get the language prefix; assets and unknown paths are never touched
minCoverage0Pages with a lower translated share are not written and get no hreflang
sitemapnoneFile to write with xhtml:link alternates
exclude200/404/500.htmlSource files to skip

Missing strings stay in the source language. The report lists them so they can go to the translation pipeline. Running again in place is idempotent.

The hydration trade-off#

With Next.js, a prerendered translated page that hydrates makes React report a text mismatch and re-render the source text, losing the translation (measured by the package's author on Next 15.5 and React 19.3). So translated pages are plain HTML without the Next runtime: they read correctly and are indexable, but the app's interactivity is lost on translated pages. The same applies to Nuxt by default. In Astro only an island hydrates its own subtree, so an island whose text is translated can mismatch: set hydration: 'strip' or mark that island translate="no". With output: 'server' only prerendered pages are translated. This is a design trade-off to know about before choosing this route.

Not covered#

Pages rendered on demand at request time, JavaScript-rendered content (the loader covers that where it runs), and shadow DOM.