brandonwie.dev
EN / KR
On this page
frontend frontendi18nsveltesveltekit

Paraglide-JS i18n for SvelteKit

Adding Korean/English internationalization to a SvelteKit static blog without

Updated March 22, 2026 6 min read

I wanted to add Korean translations to my SvelteKit blog. The site is statically generated and deployed to Cloudflare Pages, so the i18n solution needed to resolve translations at build time — not ship a runtime parser to the browser. I spent a day evaluating options and fighting with SSG compatibility before landing on Paraglide-JS. Here is everything I learned, including the gotchas the docs do not cover.


Why Most i18n Libraries Did Not Fit

The standard recommendation for i18n in JavaScript is i18next or a framework-specific wrapper like svelte-i18n. Both are solid libraries, but they share a fundamental design assumption: translations are resolved at runtime. The library ships a JSON parser to the browser, loads the active locale’s messages, and interpolates them on every render.

For a server-rendered or client-rendered app, that tradeoff is fine. For a statically generated site, it means shipping JavaScript that parses and applies translations that could have been baked into the HTML at build time. Every kilobyte matters when your hosting is a CDN edge serving pre-built files.

I needed something that compiles translations away entirely.


Options I Evaluated

OptionProsCons
Paraglide-JSTiny bundle (tree-shakes), full type safety, compile-time, native SvelteKit pluginNewer library, smaller community, docs still maturing
svelte-i18nEstablished Svelte ecosystem, runtime flexibilityRuntime parsing overhead, medium bundle, manual SvelteKit integration
i18next + svelte adapterMassive ecosystem, plugins for plurals/formattingLarge bundle, not compile-time, manual SvelteKit wiring
DIY JSON + storeNo dependency, full controlNo type safety, must build tooling from scratch

Paraglide-JS won because compile-time translation means zero runtime cost, full TypeScript support catches missing or mistyped message keys during development, and the native SvelteKit Vite plugin made integration seamless compared to manually wiring i18next.


How Paraglide Compares

This table made the decision clear:

FeatureParaglidesvelte-i18ni18next
Bundle sizeTiny (tree-shakes)MediumLarge
Type safetyFullPartialPartial
Compile-timeYesNoNo
SvelteKit integrationNativeManualManual

Tree-shaking is the key differentiator. Paraglide compiles each message into a standalone function. If you have 200 messages but a page only uses 5, only those 5 ship to the client. Runtime libraries cannot do this because they load the entire locale file.


Setting Up Paraglide-JS

Initialization takes one command:

npx @inlang/paraglide-js init

This creates three things:

  • project.inlang/settings.json — Configuration (source language, supported locales)
  • messages/en.json — English messages
  • src/lib/paraglide/ — Generated runtime (do not edit these files)

The generated runtime in src/lib/paraglide/ is opaque and auto-regenerated on every build. Resist the urge to modify it — any changes will be overwritten.


Writing Message Files

Messages live in JSON files under messages/:

// messages/en.json
{
  "site_title": "Brandon Wie | Software Engineer",
  "welcome_message": "Welcome to my blog"
}

// messages/ko.json
{
  "site_title": "Brandon Wie | 소프트웨어 엔지니어",
  "welcome_message": "블로그에 오신 것을 환영합니다"
}

Each key must exist in both files. If you add a key to en.json but forget ko.json, TypeScript will catch it at compile time — one of Paraglide’s strongest features.


Using Messages in Components

Import and call messages as functions:

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

<h1>{m.site_title()}</h1>
<p>{m.welcome_message()}</p>

Messages are functions (not strings) because they can accept parameters:

{
  "greeting": "Hello, {name}!"
}
{m.greeting({ name: 'Brandon' })}

This function-call pattern is what enables tree-shaking and type safety. The compiler knows exactly which messages each component uses and strips the rest.


Route-Based Locale Detection for SSG

For a statically generated site, cookie-based or header-based locale detection does not work — there is no server to read cookies at request time. The locale must be encoded in the URL.

I chose a prefix strategy:

/           → English (default)
/posts      → English posts
/ko         → Korean
/ko/posts   → Korean posts

English is the default and gets no prefix. Korean routes live under /ko/. This keeps English URLs clean while making the locale explicit and crawlable for search engines.

Setting the Locale in Layouts

The Korean layout sets the language tag when the route mounts:

<!-- src/routes/ko/+layout.svelte -->
<script>
  import { setLanguageTag } from '$lib/paraglide/runtime';
  setLanguageTag('ko');
</script>

<slot />

Every component under /ko/ inherits this language tag. The English routes use the default, so no explicit setLanguageTag('en') is needed.


Building a Language Toggle

The toggle component needs to construct the alternate-language URL from the current path. This sounds simple until you hit edge cases:

<script lang="ts">
  import { page } from '$app/stores';
  import { languageTag } from '$lib/paraglide/runtime';

  const currentLang = languageTag();

  // Build alternate URL
  $: currentPath = $page.url.pathname;
  $: alternateUrl = currentLang === 'en'
    ? `/ko${currentPath}`
    : currentPath.replace(/^/ko/, '') || '/';
</script>

<a href={alternateUrl}>
  {currentLang === 'en' ? '한국어' : 'English'}
</a>

The || '/' fallback at the end handles the edge case where stripping /ko from /ko leaves an empty string. Without it, the link would point to an empty href.


Vite Plugin Configuration

The Paraglide Vite plugin runs at build time, compiling messages into the generated runtime:

// vite.config.ts
import { paraglideVitePlugin } from "@inlang/paraglide-js";

export default defineConfig({
  plugins: [
    paraglideVitePlugin({
      project: "./project.inlang",
      outdir: "./src/lib/paraglide"
    })
  ]
});

One important build-order detail: the Vite plugin generates TypeScript files in src/lib/paraglide/ during the build step. If you run svelte-check before build, it will fail on missing type definitions. Always build first, then type-check.


The Difficulties I Hit Along the Way

Several issues cost me hours that the documentation did not prepare me for:

SSG compatibility gap. Most i18n examples assume server-side middleware for locale detection. Getting route-based detection to work with SvelteKit’s static adapter required reading source code rather than docs.

Generated runtime is opaque. Paraglide generates code into src/lib/paraglide/ that you should not edit. Understanding the difference between setLanguageTag (sets the active locale) and languageTag (reads the current locale) required reading the generated source since the docs were sparse at the time.

Language toggle URL construction. Building the alternate-language URL required careful pathname manipulation — stripping or adding the /ko prefix — with edge cases for the root path that only surfaced during testing.


When to Use Paraglide-JS

Paraglide fits well when:

  • You are building a SvelteKit project (especially SSG) that needs lightweight i18n
  • Type safety for translation keys matters to you
  • You have a small-to-medium translation set (personal sites, blogs, portfolios)
  • Bundle size is a priority and runtime overhead must be minimal

When to Use Something Else

  • Dynamic translations from a CMS or database — Paraglide compiles messages at build time. If translations change frequently without redeploying, a runtime library like i18next fits better.
  • Large teams with non-technical translators — Paraglide uses JSON files in the codebase. Teams that need a translation management platform (Crowdin, Lokalise) will find i18next’s ecosystem better integrated.
  • Complex ICU message syntax — Advanced pluralization, gender agreement, or nested selects go beyond Paraglide’s interpolation. i18next or FormatJS have more mature ICU support.
  • Non-SvelteKit frameworks — Paraglide’s developer experience is optimized for SvelteKit. For React/Next.js or Vue/Nuxt, use framework-native i18n solutions.

Takeaway

Paraglide-JS trades ecosystem maturity for compile-time performance. For a statically generated SvelteKit blog with two languages, that trade was worth it — zero runtime cost, full type safety, and a bundle that only includes the messages each page uses. The learning curve was steeper than expected (especially around SSG routing), but the result is an i18n setup that adds no JavaScript to the client at all.

Comments

enko