
Paraglide.js Setup: Type-Safe i18n Without Framework Lock-in
Most i18n solutions work across frameworks, but they come with tradeoffs. Runtime overhead, complex configuration, or weak TypeScript integration. You get flexibility, but you sacrifice performance or developer experience.
Paraglide.js is different. Compile-time translation generation means zero runtime overhead, full type safety, and a dead-simple API. No complex configuration, no runtime bundle bloat, no guessing which translation keys exist.
Version 2.0 introduced a framework-agnostic Vite plugin that automatically triggers builds when translations change, eliminating the manual compilation step required in v1.
I use Paraglide in production on dropanote.de, built with my own site builder. Here's why it works better than the alternatives, and how to set it up.
Core Setup
Start with the init command:
npx @inlang/paraglide-js@latest init
This creates your project configuration and translation file structure. Then add the Vite plugin to your vite.config.ts
:
import { paraglideVitePlugin } from "@inlang/paraglide-js/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
paraglideVitePlugin({
project: "./project.inlang",
outdir: "./src/lib/paraglide",
}),
],
});
Translation Files
The init command creates a messages/
directory with your translation files. Each locale gets its own JSON file:
messages/
├── en.json
└── de.json
Your English translations in messages/en.json
:
{
"hello": "Hello",
"welcome": "Welcome to our site",
"nav_home": "Home",
"nav_about": "About"
}
German translations in messages/de.json
:
{
"hello": "Hallo",
"welcome": "Willkommen auf unserer Website",
"nav_home": "Startseite",
"nav_about": "Über uns"
}
When you build your project, Paraglide generates TypeScript functions for each translation key.
Basic Usage
Start your dev server and the Vite plugin will automatically generate your translation functions:
npm run dev
Then import the generated message functions:
import * as m from "./src/lib/paraglide/messages";
Use your translations with full type safety:
// Simple messages
m.hello(); // "Hello" or "Hallo"
m.welcome(); // "Welcome to our site" or "Willkommen auf unserer Website"
// Nested keys
m.nav_home(); // "Home" or "Startseite"
m.nav_about(); // "About" or "Über uns"
For language switching, import the locale functions:
import { setLocale, getLocale } from "./src/lib/paraglide/runtime";
Switch languages programmatically:
setLocale("de");
console.log(m.hello()); // "Hallo"
setLocale("en");
console.log(m.hello()); // "Hello"
// Check current locale
console.log(getLocale()); // "en"
Your editor provides autocomplete for all translation keys, and TypeScript catches typos at compile time.
Parameters in Translations
Your translations can accept dynamic values using curly brace placeholders:
In your translation files:
{
"greeting": "Hello {name}!",
"item_count": "You have {count} items in your cart",
"user_profile": "Welcome back, {first_name} {last_name}"
}
In your code:
import * as m from "./src/lib/paraglide/messages";
m.greeting({ name: "Alice" }); // "Hello Alice!"
m.item_count({ count: 5 }); // "You have 5 items in your cart"
m.user_profile({
first_name: "John",
last_name: "Doe",
}); // "Welcome back, John Doe"
TypeScript enforces the required parameters - you'll get compile errors if you forget them or use the wrong names.
Framework Integration
The beauty of Paraglide is that it works identically across frameworks. The same import, the same functions, the same API:
import * as m from "./src/lib/paraglide/messages";
import { setLocale } from "./src/lib/paraglide/runtime";
In vanilla JavaScript:
import * as m from "./src/lib/paraglide/messages";
import { setLocale } from "./src/lib/paraglide/runtime";
document.getElementById("title").textContent = m.welcome();
// Language switching
document.getElementById("lang-de").addEventListener("click", () => {
setLocale("de");
updateUI();
});
function updateUI() {
document.getElementById("title").textContent = m.welcome();
document.getElementById("nav-home").textContent = m.nav_home();
}
In React:
function Welcome() {
return <h1>{m.welcome()}</h1>;
}
In Vue:
<template>
<h1>{{ m.welcome() }}</h1>
</template>
In Svelte:
<h1>{m.welcome()}</h1>
No framework-specific wrappers, no different APIs to learn. The same translation functions work everywhere because they're just JavaScript functions.
Advanced Configuration Options
Language Detection Strategy
Configure how Paraglide determines the user's language:
paraglideVitePlugin({
project: "./project.inlang",
outdir: "./src/lib/paraglide",
strategy: [
"url",
"cookie",
"header",
"localStorage",
"sessionStorage",
"baseLocale",
],
});
Available strategies:
url
- checks the URL path (/de/about
)cookie
- checks for a language cookieheader
- checks Accept-Language header (SSR)localStorage
- checks browser local storagesessionStorage
- checks browser session storagebaseLocale
- falls back to your default language
Paraglide tries these strategies in order until it finds a valid locale. If none match, it uses your baseLocale
.
Custom Output Directory
Change where generated files are placed:
paraglideVitePlugin({
project: "./project.inlang",
outdir: "./src/i18n", // Custom location
});
Then import from your custom path:
import * as m from "./src/i18n/messages";
Conclusion
Bundle size matters. Every kilobyte of JavaScript affects your page load times, especially on mobile devices.
Traditional i18n libraries ship their entire runtime to the browser - parsers, formatters, and configuration logic. Paraglide compiles everything at build time, so your users only download the actual translated strings as simple JavaScript functions.
This was the main reason I switched to Paraglide for my frontend-rendered sites. When every byte counts for performance, zero-runtime overhead makes a real difference.
Server-side rendering? The bundle size advantage matters less. But for client-side apps, SPAs, and any frontend-heavy project, Paraglide's compile-time approach delivers measurable performance benefits.
The type safety and framework flexibility are nice bonuses. The smaller bundles are why it's worth the switch.