# Paraglide JS: an i18n compiler that turns messages into typed functions

> Paraglide JS compiles your translation files into typed ESM functions before the bundler runs, so unused translations can be tree-shaken out of the browser bundle. The README makes a 70 percent size claim, and it points to its own benchmark page for the method behind the number.

**opral/paraglide-js** — Compiler-based i18n library that emits tree-shakable translations, leading to up to 70% smaller bundle sizes.

- Repository: https://github.com/opral/paraglide-js
- Website: https://paraglidejs.com
- Stars: 770 · Forks: 31
- Language: TypeScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/opral-paraglide-js

## A build step that happens before your bundler, not at runtime

Paraglide JS is positioned in the README as compiler-first i18n for React, TanStack Start, SvelteKit and any Vite app. The distinction that matters is where the translation work happens. A runtime i18n library ships its message catalogue to the browser and resolves a key when the code runs. Paraglide instead compiles the message files ahead of time, and the README states that the compiler turns your messages into typed ESM functions, so Vite, Rollup and other modern bundlers can tree-shake unused translations before they reach the browser. The generated code is ordinary ESM, which is why it works with the bundlers a modern Vite project already has. The target user is a team that already keeps translations in version control and wants the type system and the bundler working on the same side of the build.

## Messages start as ordinary JSON files with named placeholders

The message source is a plain JSON file per locale. The README's code preview opens with the greeting message.

```js
// messages/en.json
{
  "greeting": "Hello {name}!"
}
```

The placeholder is written inline in the string, and the compiler turns it into a function argument. That is the whole type story: the key becomes a property on the message object and the placeholder becomes a required parameter, so a typo in either is a compile error rather than a runtime surprise. The README describes this as fully typesafe with autocomplete for message keys and parameters. Locale configuration lives in project.inlang/settings.json, which configures locales, plugins and file patterns, while the translations themselves stay in ordinary files such as messages/en.json under version control. The project is built on inlang, so the open localization format is the surrounding standard rather than something Paraglide defines on its own.

## The generated m.* API is the same across frameworks

Component code imports two generated modules: the messages module and the runtime module.

```tsx
import { m } from "./paraglide/messages.js";
import { locales, setLocale } from "./paraglide/runtime.js";

export function Greeting() {
  return (
    <>
      <h1>{m.greeting({ name: "World" })}</h1> {/* fully typesafe */}
      {locales.map((locale) => (
        <button key={locale} onClick={() => setLocale(locale)}>
          {locale}
        </button>
      ))}
    </>
  );
}
```

The runtime module exports the list of available locales and a setter, which is enough to build a language switcher without touching the message files. The README is explicit that the same m.* API works in SvelteKit, TanStack Start, React Router, Vue, Solid, Astro and vanilla JavaScript or TypeScript, and that only the surrounding component code changes. That portability claim is the reason the package describes itself as Vite-native, and the repository backs it with example directories for astro, cli, formatting, incremental-migration, next-js-ssg, next-js-ssr, react-router, react, sveltekit, tanstack-start and vite.

## Where the 70 percent size claim comes from, and how to read it

The README claims up to 70 percent smaller i18n bundle sizes compared to runtime i18n libraries, and it does the honest thing by naming the configuration: 47 KB against 205 KB for 5 locales and 200 messages, with a link to a benchmark page for the methodology. Treat that as a project claim rather than a guarantee. The mechanism behind it is the same in every case: compiled ESM functions that a bundler can drop when they are unreferenced. Your own numbers depend on how many locales and messages you have, and on whether the runtime library you compare against also ships only what you import. The repository contains a benchmark directory and a bench script wired to vitest bench, plus an ax-eval-report.md file, so the comparison is at least reproducible in the project rather than only in prose. What the compiler cannot shrink is the locale you do use, since those messages still have to reach the browser.

## Locale routing is built in, but the file is not the only configuration

The feature table lists built-in i18n routing with URL-based locale detection and localized paths out of the box. That is convenient, but it also means the library is opinionated about how a URL maps to a locale, and the README excerpt does not spell out the details of that mapping. The configuration surface is split in two places, and this is worth understanding before adopting the library: project.inlang/settings.json describes locales, plugins and file patterns, while messages/en.json holds the strings. A project with an existing message catalogue has to fit that catalogue into the file patterns inlang expects. The framework integrations are handled on the framework side rather than by Paraglide itself. The README describes SvelteKit's official i18n integration as framework-authored and framework-tested, and points at an end-to-end tested i18n example in TanStack Router.

## What the package actually ships, and how you install it

The README does not give an installation command, so there is nothing to copy here, and the README points to the documentation site paraglidejs.com and to a quick start section instead. What the package manifest shows is a workspace package named @inlang/paraglide-js, at version 2.29.1, published with public access, and it registers a command line binary called paraglide-js pointing at ./bin/run.js. The published files are the dist directory and the bin directory, and the exports map exposes the main entry, a urlpattern-polyfill subpath and an eslint subpath. The urlpattern polyfill export is a practical hint about how locale routing works, since URLPattern is not available in every runtime, and shipping a polyfill subpath means the project has thought about older browsers. The eslint subpath suggests a lint rule for catching message key mistakes in code that the compiler does not check.

## MIT licensed, moving quickly, and released through changesets

The licence is MIT, declared in the package manifest, so the code can be used in commercial products under the usual notice requirement. The repository is not archived and the last push was on 2026-10-19. Versioning runs through changesets, with a .changeset directory, a changeset script and a version-packages script in the manifest, and the CHANGELOG.md file at the root. The project uses pnpm workspaces, with a pnpm-workspace.yaml and a pnpm-lock.yaml, and the framework packages live under a framework directory with their own build and test steps. One caveat for anyone tracking releases: the repository has no GitHub releases, so the npm version in package.json and the CHANGELOG are the places to look. The CI script builds the package, the framework packages, the examples and the website, then runs every test suite, which is a sign that the framework integrations are tested rather than merely documented.

## Conclusion

Paraglide JS fits teams on Vite with version controlled JSON message files who want type safe message keys and a smaller browser payload than a runtime lookup library will give them. It does not fit projects without a build step, or teams that need message files in a format other than the ones inlang handles. Verify first that the tree-shaking claim holds in your own build, by checking the emitted chunk size for a locale you do not use, and confirm that the compile step fits your deployment before you migrate a large message catalogue.

## FAQ

### How do I install Paraglide JS?

The README does not include an install command and instead points to the documentation at paraglidejs.com and a quick start section. The npm package is @inlang/paraglide-js, and the package manifest registers a paraglide-js binary.

### Which frameworks does Paraglide JS support?

The README describes it as compiler-first i18n for React, TanStack Start, SvelteKit and any Vite app, and says the same m.* API works in React Router, Vue, Solid, Astro and vanilla JavaScript or TypeScript.

### How does Paraglide JS keep unused translations out of the bundle?

The compiler turns messages into typed ESM functions, so the bundler can tree-shake translations that are never referenced. The README claims up to 70 percent smaller i18n bundle sizes, citing 47 KB against 205 KB for 5 locales and 200 messages, and links to a benchmark page for the method.

### Where are translation files and locale settings kept in Paraglide JS?

Translations stay in version controlled files such as messages/en.json, while locales, plugins and file patterns are configured in project.inlang/settings.json. The project is built on the inlang open localization format.

## Sources

- [Issues](https://github.com/opral/paraglide-js/issues)
- [License: MIT](https://github.com/opral/paraglide-js/blob/main/LICENSE)
- [opral/paraglide-js on GitHub](https://github.com/opral/paraglide-js)
- [Project website](https://paraglidejs.com)
- [README](https://github.com/opral/paraglide-js/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/opral-paraglide-js
