Introduction
Shipping a SvelteKit application to users across different regions means more than just translating text—you need to handle locale-specific formatting, SEO concerns, and a seamless experience regardless of where someone accesses your site. Internationalization in SvelteKit in Svelte & SvelteKit brings together message management, routing, and standards compliance into a cohesive system.
This article walks through production patterns we've used across ride-hailing and travel platforms: type-safe message handling with Paraglide, locale routing strategies, SEO considerations with hreflang tags, and proper date and number formatting. By the end, you'll have a blueprint for shipping multi-language applications that scale.
Paraglide for Type-Safe Messages
Paraglide is a lightweight i18n compiler that generates fully typed message functions for your SvelteKit app. Unlike traditional i18n libraries that load JSON at runtime, Paraglide compiles messages into JavaScript, eliminating bundle bloat and making it impossible to reference a message that doesn't exist.
Let's start with a basic setup. First, install Paraglide and its SvelteKit integration:
npm install @inlang/paraglide-js @inlang/paraglide-js-adapter-sveltekitNext, create a messages directory at your project root with language subdirectories:
messages/
├── en/
│ ├── common.json
│ └── errors.json
└── es/
├── common.json
└── errors.jsonIn messages/en/common.json:
{
"welcome": "Welcome, {name}",
"checkout_total": "Total: {amount, number}",
"no_results": "No results found"
}In messages/es/common.json:
{
"welcome": "Bienvenido, {name}",
"checkout_total": "Total: {amount, number}",
"no_results": "Sin resultados"
}Configure Paraglide in svelte.config.js:
import adapter from '@sveltejs/adapter-auto';
import { paraglide } from '@inlang/paraglide-js-adapter-sveltekit/vite';
export default {
kit: {
adapter: adapter(),
},
vite: {
plugins: [
paraglide({
project: './project.inlang',
outdir: './src/lib/paraglide',
}),
],
},
};Now use messages in your components with full type safety:
<script>
import * as m from '$lib/paraglide/messages';
import { languageTag } from '$lib/paraglide/runtime';
export let name = 'Alice';
export let amount = 1299.50;
</script>
<h1>{m.welcome({ name })}</h1>
<p>{m.checkout_total({ amount })}</p>TypeScript will error if you pass the wrong parameters or reference a message that doesn't exist. This catches i18n bugs at build time rather than in production.
Locale in URL or Cookie
You need to decide where locale lives: in the URL path (preferred for SEO) or in cookies (simpler for authenticated users). Most teams use URL-based routing for public content and cookies as a fallback for logged-in experiences.
For URL-based routing, structure routes by language prefix:
src/routes/
├── [lang]/
│ ├── +layout.svelte
│ ├── +layout.server.ts
│ ├── index.svelte
│ └── checkout/
│ └── +page.svelte
└── +page.svelte (redirect)In src/routes/[lang]/+layout.server.ts, validate and set the locale:
import { redirect } from '@sveltejs/kit';
import { setLanguageTag } from '$lib/paraglide/runtime';
const SUPPORTED_LANGUAGES = ['en', 'es', 'fr', 'de'];
export async function load({ params }) {
const lang = params.lang;
if (!SUPPORTED_LANGUAGES.includes(lang)) {
redirect(307, '/en');
}
setLanguageTag(lang);
return { lang };
}Create a catch-all root route that detects browser locale and redirects:
// src/routes/+page.server.ts
import { redirect } from '@sveltejs/kit';
export async function load({ request }) {
const acceptLanguage = request.headers.get('accept-language') || 'en';
const preferredLang = acceptLanguage.split(',')[0].split('-')[0];
const supportedLangs = ['en', 'es', 'fr', 'de'];
const lang = supportedLangs.includes(preferredLang) ? preferredLang : 'en';
redirect(307, `/${lang}`);
}For authenticated users, store language preference in a cookie and check it server-side:
// In your login/settings handler
export async function POST({ cookies, request }) {
const data = await request.json();
if (data.language) {
cookies.set('preferred_lang', data.language, {
path: '/',
maxAge: 60 * 60 * 24 * 365, // 1 year
sameSite: 'strict',
});
}
}Then check the cookie before the URL parameter:
export async function load({ params, cookies }) {
let lang = params.lang;
// Check cookie if no lang in URL
if (!lang) {
lang = cookies.get('preferred_lang') || 'en';
}
const supportedLangs = ['en', 'es', 'fr', 'de'];
if (!supportedLangs.includes(lang)) {
lang = 'en';
}
setLanguageTag(lang);
return { lang };
}hreflang Tags for SEO
Search engines need to understand your multi-language content. The hreflang tag tells Google and other crawlers which version of a page is intended for each language and region, preventing duplicate content penalties and ensuring the right version appears in search results.
Add hreflang tags in your layout's head. Create a utility to generate them:
// src/lib/i18n/hreflang.ts
export function generateHreflangs(url: string, availableLanguages: string[]) {
const hreflangs = availableLanguages.map(lang => ({
lang,
href: url.replace(/\/[a-z]{2}\//, `/${lang}/`),
}));
// Add x-default for no specific language preference
hreflangs.push({
lang: 'x-default',
href: url.replace(/\/[a-z]{2}\//, '/'),
});
return hreflangs;
}Use it in your root layout:
<script>
import { page } from '$app/stores';
import { generateHreflangs } from '$lib/i18n/hreflang';
const availableLanguages = ['en', 'es', 'fr', 'de'];
$: hreflangs = generateHreflangs($page.url.pathname, availableLanguages);
</script>
<svelte:head>
{#each hreflangs as link}
<link rel="alternate" hreflang="{link.lang}" href="{link.href}" />
{/each}
</svelte:head>For language-specific sitemap.xml, add one for each language in your build output or serve them dynamically. This accelerates SEO adoption and improves indexing across all your language variants.
Format Dates and Numbers Per Locale
Raw numbers and dates confuse international users. 2/3 means March 2nd in the US but February 3rd in Europe. Numbers use different separators: 1.234,56 in Germany versus 1,234.56 in the US.
Use the browser's native Intl API, which requires no extra dependencies:
// src/lib/i18n/format.ts
export function formatDate(date: Date, locale: string): string {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
}
export function formatNumber(value: number, locale: string): string {
return new Intl.NumberFormat(locale).format(value);
}
export function formatCurrency(value: number, locale: string, currency: string): string {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).format(value);
}
export function formatTime(date: Date, locale: string): string {
return new Intl.DateTimeFormat(locale, {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}).format(date);
}Use these in components:
<script>
import { languageTag } from '$lib/paraglide/runtime';
import { formatDate, formatCurrency, formatNumber } from '$lib/i18n/format';
const orderDate = new Date('2025-02-15');
const total = 12999.99;
const quantity = 150000;
</script>
<p>Order placed: {formatDate(orderDate, $languageTag)}</p>
<p>Total: {formatCurrency(total, $languageTag, 'EUR')}</p>
<p>Items sold: {formatNumber(quantity, $languageTag)}</p>In server-side rendering contexts, specify the locale explicitly to avoid hydration mismatches:
export async function load({ params }) {
const locale = params.lang || 'en';
return {
formattedDate: formatDate(new Date(), locale),
formattedPrice: formatCurrency(9999, locale, 'USD'),
};
}Common Mistakes
Teams often run into these traps when shipping multi-language apps:
- Hardcoding strings: Even "common" labels like "OK" or "Cancel" should be translatable. Translators need consistency across the app.
- Forgetting to set hreflang on every page: Missing hreflang on some pages confuses search engines. Generate it programmatically to ensure it's never missed.
- Locale mismatch between server and client: If your server renders with locale 'en' but the client initializes with locale 'es', you'll see hydration errors. Always pass locale through context or stores.
- Not testing with actual translations: A 20-character English phrase might become 40 characters in German. UI can break. Test layouts with expanded text early.
- Ignoring RTL languages: If you expand to Arabic or Hebrew, CSS margins and paddings reverse. Use logical properties (
margin-inline-startinstead ofmargin-left). - Storing locale in multiple places: If you have locale in both a cookie and URL, they can drift. Pick one source of truth and sync from there.
Key Takeaways
- Paraglide provides type-safe, compiled message functions that catch missing or incorrect message references at build time, eliminating runtime i18n bugs.
- URL-based locale routing improves SEO and gives each language its own canonical URL; combine with cookies for authenticated users to remember preferences.
- hreflang tags prevent duplicate content penalties and guide search engines to the correct language version for each user.
- Use the native
IntlAPI to format dates, numbers, and currencies per locale without additional dependencies or maintenance. - Keep locale consistent between server and client to avoid hydration mismatches; pass it through layout servers and context stores.
- Test with real translated content early, particularly around layout expansion and RTL language support if you plan to expand globally.






Comments
Loading comments…