Skip to content

Fix: Paraglide Not Working — Messages Not Loading, Compiler Errors, or Framework Integration Issues

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix Paraglide.js i18n issues — message compilation, type-safe translations, SvelteKit and Next.js integration, language switching, and message extraction from existing code.

The Problem

Paraglide messages are undefined:

import * as m from '$lib/paraglide/messages';

m.welcome();
// TypeError: m.welcome is not a function

Or the compiler fails:

npx @inlang/paraglide-js compile
# Error: No messages found in project

Or language switching doesn’t update the page:

Language changes in the dropdown but text stays in English

Why This Happens

Paraglide.js is a compile-time i18n library that generates type-safe message functions from translation files. Key differences from runtime i18n:

  • Messages are compiled to JavaScript functions — each translation becomes a function like welcome() that returns the translated string for the current locale. If compilation hasn’t run, the functions don’t exist.
  • An inlang project file is required — Paraglide reads messages from a project.inlang/ directory containing settings.json and message files. Without proper project setup, the compiler finds nothing.
  • Language state must be managed per framework — Paraglide doesn’t manage routing or language detection. Framework adapters (SvelteKit, Next.js) handle URL-based language routing and provide the current locale to Paraglide.
  • Tree-shaking removes unused translations — only messages actually imported and called end up in the bundle. This is a feature, but it means unused messages aren’t just “available at runtime.”

The deeper reason this trips so many teams is that Paraglide’s compile step is invisible. There is no runtime registry to inspect, no async init() you forgot to await — the compiler simply emits files into outdir, and at import time either the file exists or it does not. When you copy a working setup from a SvelteKit project into a Next.js or Astro project, the Vite plugin happens to compile on dev-server boot in SvelteKit because Vite drives the dev loop. In Next.js the App Router only runs Vite-style plugins through its own webpack/turbopack wrappers, so the compile step has to be hooked into next dev. In Astro, astro:build runs the compile but astro dev may not unless the integration is wired up. Without the right plugin for the right framework, outdir is empty and every message function is undefined.

The other systemic issue is locale state across runtimes. Paraglide’s languageTag() reads from an AsyncLocalStorage-backed runtime on the server and from a module-scoped variable in the browser. In SvelteKit, the i18n.handle() hook installs the server context per request. In Next.js, middleware sets a header that the server runtime reads inside RSC. On Astro the integration writes the locale into Astro.locals. If you SSR a page without going through the right adapter — for example, a Next.js API route that calls a translation function directly — you get the default locale even though the URL says /ja/. The fix is always to route through the framework’s request handler, not to import the runtime directly in places the adapter does not cover.

Fix 1: Basic Setup

npx @inlang/paraglide-js init
# Creates project.inlang/ directory
// project.inlang/settings.json
{
  "$schema": "https://inlang.com/schema/project-settings",
  "sourceLanguageTag": "en",
  "languageTags": ["en", "ja", "es", "fr"],
  "modules": [
    "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@latest/dist/index.js",
    "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@latest/dist/index.js",
    "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js",
    "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js"
  ],
  "plugin.inlang.messageFormat": {
    "pathPattern": "./messages/{languageTag}.json"
  }
}
// messages/en.json
{
  "$schema": "https://inlang.com/schema/inlang-message-format",
  "welcome": "Welcome to our app",
  "greeting": "Hello, {name}!",
  "items_count": "{count, plural, =0 {No items} one {# item} other {# items}}",
  "nav_home": "Home",
  "nav_about": "About",
  "nav_contact": "Contact"
}

// messages/ja.json
{
  "$schema": "https://inlang.com/schema/inlang-message-format",
  "welcome": "アプリへようこそ",
  "greeting": "こんにちは、{name}さん!",
  "items_count": "{count, plural, other {{count}個のアイテム}}",
  "nav_home": "ホーム",
  "nav_about": "概要",
  "nav_contact": "お問い合わせ"
}
# Compile messages to JavaScript functions
npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide
// Generated: src/lib/paraglide/messages.ts
// Each message becomes a type-safe function:
// export function welcome(): string
// export function greeting(params: { name: string }): string
// export function items_count(params: { count: number }): string

Fix 2: SvelteKit Integration

npm install @inlang/paraglide-sveltekit
// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { paraglide } from '@inlang/paraglide-sveltekit/vite';

export default defineConfig({
  plugins: [
    paraglide({
      project: './project.inlang',
      outdir: './src/lib/paraglide',
    }),
    sveltekit(),
  ],
});
// src/lib/i18n.ts
import { createI18n } from '@inlang/paraglide-sveltekit';
import * as runtime from '$lib/paraglide/runtime';

export const i18n = createI18n(runtime, {
  pathnames: {
    '/about': {
      en: '/about',
      ja: '/about',
      es: '/acerca-de',
    },
  },
  exclude: ['/api'],
});
// src/hooks.ts
import { i18n } from '$lib/i18n';
import type { Handle } from '@sveltejs/kit';

export const handle: Handle = i18n.handle();
// src/routes/+layout.ts
import { i18n } from '$lib/i18n';
import type { LayoutLoad } from './$types';

export const load: LayoutLoad = ({ url }) => {
  return i18n.layoutLoad({ url });
};
<!-- src/routes/+page.svelte -->
<script>
  import * as m from '$lib/paraglide/messages';
  import { i18n } from '$lib/i18n';
  import { languageTag } from '$lib/paraglide/runtime';
</script>

<h1>{m.welcome()}</h1>
<p>{m.greeting({ name: 'Alice' })}</p>
<p>{m.items_count({ count: 5 })}</p>

<!-- Language switcher -->
<nav>
  <a href={i18n.route($page.url.pathname)} hreflang="en">English</a>
  <a href={i18n.route($page.url.pathname)} hreflang="ja">日本語</a>
  <a href={i18n.route($page.url.pathname)} hreflang="es">Español</a>
</nav>

<p>Current: {languageTag()}</p>

Fix 3: Next.js Integration

npm install @inlang/paraglide-next
// next.config.mjs
import { paraglide } from '@inlang/paraglide-next/plugin';

export default paraglide({
  paraglide: {
    project: './project.inlang',
    outdir: './src/lib/paraglide',
  },
  // Next.js config
});
// src/lib/i18n.ts
import { createI18n } from '@inlang/paraglide-next';
import * as runtime from '@/lib/paraglide/runtime';

export const { middleware, Link, useRouter, usePathname, redirect, permanentRedirect } =
  createI18n(runtime);
// middleware.ts
export { middleware } from '@/lib/i18n';

export const config = {
  matcher: ['/((?!api|_next|.*\\..*).*)'],
};
// app/[lang]/layout.tsx
import { LanguageProvider } from '@inlang/paraglide-next';
import { languageTag } from '@/lib/paraglide/runtime';

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <LanguageProvider>
      <html lang={languageTag()}>
        <body>{children}</body>
      </html>
    </LanguageProvider>
  );
}
// app/[lang]/page.tsx — use translations
import * as m from '@/lib/paraglide/messages';
import { Link } from '@/lib/i18n';

export default function Home() {
  return (
    <div>
      <h1>{m.welcome()}</h1>
      <p>{m.greeting({ name: 'Alice' })}</p>

      {/* Type-safe Link with locale handling */}
      <Link href="/about">
        {m.nav_about()}
      </Link>
    </div>
  );
}

Fix 4: Type-Safe Message Parameters

// messages/en.json
{
  "order_status": "Order #{orderId} is {status}",
  "price": "Total: {amount, number, ::currency/USD}",
  "last_login": "Last login: {date, date, medium}",
  "file_size": "{size, number, ::compact-short} bytes",
  "progress": "{percent, number, ::percent} complete"
}
// Generated functions are fully typed:
import * as m from '$lib/paraglide/messages';

m.order_status({ orderId: '12345', status: 'shipped' });
// "Order #12345 is shipped"

m.greeting({ name: 'Alice' });
// TypeScript error if name is missing:
// m.greeting({})  // Error: Property 'name' is missing

// Unused messages are tree-shaken from the bundle
// Only imported messages end up in production code

Fix 5: Development Workflow

# Compile messages (run after editing .json files)
npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide

# Watch mode — recompile on message changes
npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide --watch

# With the Vite plugin, compilation is automatic during dev
// package.json
{
  "scripts": {
    "i18n:compile": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide",
    "dev": "npm run i18n:compile && vite dev",
    "build": "npm run i18n:compile && vite build",
    "i18n:lint": "npx @inlang/cli lint --project ./project.inlang"
  }
}
# Lint translations — find missing or empty messages
npx @inlang/cli lint --project ./project.inlang

# Output:
# Missing translation for "nav_contact" in "ja"
# Empty pattern for "description" in "es"

Fix 6: Using the Inlang Editor

# Visual editor for translations (VS Code extension or web app)
# Install: Sherlock — i18n inspector (VS Code extension)

# Or use the Fink web editor:
# Open: https://fink.inlang.com
# Load your project.inlang/ directory
# Translators can edit messages in a web UI

Fix 7: Platform-Specific Adapters — SvelteKit, Next.js, Astro, and Edge

Paraglide ships separate packages per framework. Pick the one that matches your adapter and make sure the Vite plugin is the first plugin so it runs before route generation.

// SvelteKit — paraglide must run before sveltekit()
import { paraglide } from '@inlang/paraglide-sveltekit/vite';
import { sveltekit } from '@sveltejs/kit/vite';

export default defineConfig({
  plugins: [paraglide({ project: './project.inlang', outdir: './src/lib/paraglide' }), sveltekit()],
});
// Astro — register as an integration, not as a Vite plugin
import paraglide from '@inlang/paraglide-astro';

export default defineConfig({
  integrations: [
    paraglide({ project: './project.inlang', outdir: './src/paraglide' }),
  ],
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'ja'],
    routing: { prefixDefaultLocale: false },
  },
});
// Next.js — wrap next.config with the paraglide plugin
import { paraglide } from '@inlang/paraglide-next/plugin';
export default paraglide({ paraglide: { project: './project.inlang', outdir: './src/paraglide' } });

JSON catalog format quirks: the plugin-message-format plugin expects ICU MessageFormat inside JSON values. Plain Markdown or HTML inside a message will pass compile but render as raw text. For rich content, split the message into segments and wrap them in JSX/Svelte on the consuming side, or use the paraglide-markdown style helpers. Newer Paraglide releases support nested keys; older ones do not. If your VS Code Sherlock extension shows “invalid message” warnings, the catalog plugin is treating a nested object as a single message.

SSG vs SSR locale routing: SvelteKit and Astro both support pre-rendering per locale. The Paraglide SvelteKit adapter rewrites URLs in i18n.handle() so a static build produces /, /ja/, /es/. For Next.js App Router, the [lang] segment must be a dynamic segment, and generateStaticParams must list every locale. SSR-only deployments (Vercel Edge, Cloudflare Pages with SSR) need the middleware to set the locale before the RSC tree renders.

Cloudflare Workers / KV-backed translations: Paraglide compiles to static JS, so all translations ship in the bundle by default. If your translation set is huge and you want to load translations from KV at request time, you cannot use Paraglide’s compiled messages.ts — you would have to call i18n.loadMessages() dynamically, which defeats tree-shaking. The pragmatic answer: keep UI strings in Paraglide and load CMS-style content (long-form copy, product descriptions) from KV separately.

Vite plugin requirement is hard: Paraglide does not have a runtime-only mode. If your framework does not run Vite — for example a custom esbuild setup or a raw Node.js server — call paraglide-js compile from package.json as a prebuild and predev script. Without the plugin, edits to messages/*.json will not trigger recompilation and you will keep seeing stale strings until you re-run the command.

Still Not Working?

m.welcome is not a function — the paraglide output hasn’t been compiled. Run npx @inlang/paraglide-js compile or ensure the Vite/Next.js plugin is configured. The outdir must match your import path ($lib/paraglide for SvelteKit, @/lib/paraglide for Next.js).

Compiler finds no messagesproject.inlang/settings.json must point to the correct message files. Check plugin.inlang.messageFormat.pathPattern matches where your .json files are. The default is ./messages/{languageTag}.json.

Language switching doesn’t update text — Paraglide needs the framework adapter to manage the current locale. In SvelteKit, use i18n.handle() in hooks. In Next.js, use the Paraglide middleware. Without the adapter, languageTag() always returns the default.

Bundle still includes all languages — Paraglide tree-shakes unused messages but includes all languages for used messages. This is by design — the current locale is determined at runtime. The per-language overhead is minimal (just the translated strings, no library code).

Astro astro dev shows English even with /ja/ URL — the Astro integration is missing or the i18n block in astro.config.mjs does not list ja. The Paraglide integration only writes the locale into Astro.locals for routes that Astro’s own i18n config considers localized.

Next.js RSC pages render the default locale on first paint, then switch — middleware ran but you called languageTag() inside a Client Component that hydrated before the provider mounted. Read languageTag() inside the Server Component layout and pass the result down as a prop, or use the LanguageProvider from @inlang/paraglide-next.

ICU plural / select rules render the literal placeholder — your message file did not pass through the plugin parser, usually because the JSON has a typo in the ICU syntax. Run npx @inlang/cli lint --project ./project.inlang to surface malformed messages before they hit the compiler.

For related i18n issues, see Fix: i18next Not Working, Fix: Lingui Not Working, Fix: next-intl Not Working, and Fix: SvelteKit Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles