
Localization Engineer
- 55 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
localization-engineer is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- localization-engineer
- AI & Agent Building
- AI-coding skill
Localization Engineer by the numbers
- 55 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #6,763 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill localization-engineerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 55 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Localization Engineer
Overview
Provides framework-agnostic patterns for building multilingual applications, covering the full i18n pipeline from locale detection through translation rendering. Addresses universal concerns like ICU message formatting, CLDR plural rules, RTL support, and SEO.
When to use: Adding multi-language support, locale-aware formatting, RTL layouts, pluralization, translation management, locale negotiation, multilingual SEO.
When NOT to use: Single-language apps with no internationalization plans, content that never changes locale, purely server-rendered static sites with no dynamic content.
Key decision: Choose Paraglide for compile-time type safety and minimal bundles in new projects. Choose i18next for broad ecosystem support and incremental adoption in existing projects. See the Library Selection table below.
Library Selection
| Criteria | Paraglide JS | i18next |
|---|---|---|
| Architecture | Compile-time, tree-shakable functions | Runtime, plugin-based ecosystem |
| Type safety | Full (generated typed message functions) | Partial (requires manual type setup) |
| Bundle size | Minimal (only used messages shipped) | Larger runtime (~40 kB base + plugins) |
| Pluralization | Via message format plugins | Built-in with CLDR plural categories |
| Framework support | SvelteKit, TanStack Start, React Router, Astro, vanilla | React, Vue, Svelte, Angular, Node, vanilla |
| ICU support | Via inlang-icu-messageformat-1 plugin | Via i18next-icu plugin |
| Translation tools | Fink editor, Sherlock VS Code extension | Locize, i18next-parser, many integrations |
| Locale strategies | Built-in (cookie, URL, header, custom) | Via i18next-browser-languagedetector plugin |
| SSR support | AsyncLocalStorage-based per-request | Separate i18next instance per request |
| Learning curve | Low (compiler generates simple functions) | Medium (large API surface, many plugins) |
| Namespace support | Flat message files per locale | Multi-namespace with lazy loading per route |
| Message format | Inlang format or ICU via plugin | i18next JSON v4 or ICU via plugin |
| Community size | Growing (inlang ecosystem) | Large (established since 2015) |
| Best for | New projects prioritizing bundle size and type safety | Existing projects needing broad ecosystem support |
Quick Reference
| Pattern | Approach | Key Points |
|---|---|---|
| ICU pluralization | {count, plural, one {# item} other {# items}} | CLDR categories: zero, one, two, few, many, other |
| ICU select (gender) | {gender, select, male {He} female {She} other {They}} | Covers gender-aware and variant-based messages |
| ICU selectordinal | {rank, selectordinal, one {#st} two {#nd} other {#th}} | Ordinal suffixes vary by language |
| Number formatting | Intl.NumberFormat(locale, options) | Currency, percent, unit formatting built into platform |
| Date formatting | Intl.DateTimeFormat(locale, options) | Avoid manual date string construction |
| Relative time | Intl.RelativeTimeFormat(locale, options) | "3 hours ago", "in 2 days" with locale-aware output |
| List formatting | Intl.ListFormat(locale, { type: 'conjunction' }) | "Alice, Bob, and Charlie" with locale-aware conjunctions |
| RTL layout | CSS logical properties (inline-start, inline-end) | Replace left/right with logical equivalents |
| RTL detection | <html dir="rtl" lang="ar"> | Set dir attribute based on locale |
| Locale from URL | /en/about, /de/about | Most SEO-friendly, clear to users |
| Locale from cookie | locale=de cookie | Persists preference across sessions |
| Locale from header | Accept-Language: de-DE,de;q=0.9,en;q=0.8 | Browser preference, use as fallback |
| Hreflang tags | <link rel="alternate" hreflang="de" href="..."> | One per locale plus x-default |
| Namespace splitting | Group translations by feature or route | Reduces bundle size via lazy loading |
| Message extraction | Automated tooling scans source for translation keys | Prevents orphaned or missing translations |
| Pseudolocalization | Generate fake translations to test layout | Catches truncation, overflow, hardcoded strings |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
| Concatenating translated fragments into sentences | Use ICU message format with placeholders for proper grammar across locales |
Using margin-left/padding-right with RTL | Use CSS logical properties (margin-inline-start, padding-inline-end) |
Hardcoding plural rules (count === 1) | Use CLDR plural categories (some languages have zero, two, few, many forms) |
| Detecting locale only from IP geolocation | Use Accept-Language header, then cookie, then geolocation as last resort |
Missing x-default in hreflang tags | Always include <link rel="alternate" hreflang="x-default"> for fallback |
| One massive translation file per locale | Split into namespaces by feature for lazy loading and maintainability |
| Creating new i18next instance per component | Create once at app initialization, share across components |
| Formatting dates with string concatenation | Use Intl.DateTimeFormat for locale-aware date rendering |
| Storing locale in component state | Store in URL path or cookie for SSR compatibility and shareability |
| Using different i18n keys in client and server | Share translation files and key structure between client and server |
| Assuming all languages expand equally | Budget 30-40% extra space for German/Finnish translations vs English |
| Embedding text in images | Use CSS/SVG text overlays so translations can be applied dynamically |
| Using language codes without region variants | Use en-US vs en-GB when formatting differences matter (dates, currency) |
Delegation
- Audit codebase for hardcoded strings: Use
Exploreagent to scan components for untranslated user-facing text - Set up full i18n pipeline: Use
Taskagent to configure locale detection, translation loading, and rendering - Plan localization architecture: Use
Planagent to design namespace structure, locale strategy, and translation workflow - Review translation coverage: Use
Taskagent to compare translation files and identify missing keys across locales - Code review for i18n compliance: Delegate to
code-revieweragent
If the tanstack-start skill is available, delegate server middleware and SSR locale patterns to it.If the tanstack-router skill is available, delegate URL-based locale routing patterns to it.If the sveltekit skill is available, delegate SvelteKit-specific routing and hooks patterns to it.If the seo skill is available, delegate advanced hreflang and sitemap patterns to it.References
- Paraglide JS patterns -- compile-time i18n with typed message functions, SvelteKit/TanStack Start/React Router integration, locale strategies
- i18next patterns -- runtime i18n with plugin ecosystem, React/Vue/Svelte/vanilla integration, namespaces, language detection
- ICU message format -- pluralization, select, number/date formatting, nested messages, RTL support, hreflang SEO
i18next
i18next is a runtime internationalization framework for JavaScript. Its plugin architecture supports language detection, translation loading, post-processing, and framework bindings for React, Vue, Svelte, Angular, Node.js, and vanilla JS.
Installation
npm install i18nextFramework-specific bindings:
npm install react-i18next
npm install i18next-vue
npm install svelte-i18nextBasic Configuration
import i18next from 'i18next';
await i18next.init({
lng: 'en',
fallbackLng: 'en',
debug: false,
interpolation: {
escapeValue: false,
},
resources: {
en: {
translation: {
greeting: 'Hello, {{name}}!',
item_count_one: '{{count}} item',
item_count_other: '{{count}} items',
},
},
de: {
translation: {
greeting: 'Hallo, {{name}}!',
item_count_one: '{{count}} Artikel',
item_count_other: '{{count}} Artikel',
},
},
},
});
i18next.t('greeting', { name: 'World' });
i18next.t('item_count', { count: 5 });Translation File Structure (JSON v4)
{
"greeting": "Hello, {{name}}!",
"nested": {
"key": "Nested value"
},
"reuse": "This reuses $t(greeting, {\"name\": \"World\"})",
"unescaped": "Raw HTML: {{- value}}",
"formatted": "Price: {{value, number}}",
"context_male": "He liked this",
"context_female": "She liked this",
"count_one": "{{count}} item",
"count_other": "{{count}} items",
"count_zero": "No items",
"count_two": "{{count}} items",
"count_few": "{{count}} items",
"count_many": "{{count}} items"
}Pluralization
i18next uses CLDR plural categories as key suffixes:
{
"cart_one": "You have {{count}} item in your cart",
"cart_other": "You have {{count}} items in your cart"
}For languages with more plural forms (e.g., Arabic):
{
"cart_zero": "...",
"cart_one": "...",
"cart_two": "...",
"cart_few": "...",
"cart_many": "...",
"cart_other": "..."
}Usage:
i18next.t('cart', { count: 0 });
i18next.t('cart', { count: 1 });
i18next.t('cart', { count: 5 });Namespaces
Split translations by feature to enable lazy loading:
await i18next.init({
ns: ['common', 'dashboard', 'settings'],
defaultNS: 'common',
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
});
i18next.t('save_button');
i18next.t('dashboard:chart_title');
i18next.t('settings:theme_label');Load namespaces on demand:
await i18next.loadNamespaces('settings');Language Detection (Browser)
npm install i18next-browser-languagedetectorimport i18next from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
i18next.use(LanguageDetector).init({
fallbackLng: 'en',
detection: {
order: ['querystring', 'cookie', 'localStorage', 'navigator', 'htmlTag'],
lookupQuerystring: 'lng',
lookupCookie: 'i18next',
lookupLocalStorage: 'i18nextLng',
caches: ['cookie', 'localStorage'],
},
});Backend Loading
npm install i18next-http-backendimport i18next from 'i18next';
import Backend from 'i18next-http-backend';
i18next.use(Backend).init({
fallbackLng: 'en',
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
addPath: '/locales/add/{{lng}}/{{ns}}',
},
});React Integration
import i18next from 'i18next';
import { initReactI18next, useTranslation, Trans } from 'react-i18next';
i18next.use(initReactI18next).init({
lng: 'en',
fallbackLng: 'en',
interpolation: { escapeValue: false },
resources: {
en: { translation: { greeting: 'Hello, {{name}}!' } },
},
});
function Greeting() {
const { t, i18n } = useTranslation();
return (
<div>
<h1>{t('greeting', { name: 'World' })}</h1>
<button onClick={() => i18n.changeLanguage('de')}>Deutsch</button>
</div>
);
}Rich Text with Trans Component
import { Trans } from 'react-i18next';
function Terms() {
return (
<Trans i18nKey="terms">
By signing up, you agree to our <a href="/terms">Terms</a> and{' '}
<a href="/privacy">Privacy Policy</a>.
</Trans>
);
}Lazy-Loading Namespaces in React
import { useTranslation } from 'react-i18next';
function Settings() {
const { t, ready } = useTranslation('settings', { useSuspense: false });
if (!ready) return <div>Loading...</div>;
return <h1>{t('page_title')}</h1>;
}Vue Integration
import i18next from 'i18next';
import I18NextVue from 'i18next-vue';
import { createApp } from 'vue';
import App from './App.vue';
await i18next.init({
lng: 'en',
resources: {
en: { translation: { greeting: 'Hello, {{name}}!' } },
},
});
const app = createApp(App);
app.use(I18NextVue, { i18next });
app.mount('#app');<template>
<h1>{{ $t('greeting', { name: 'World' }) }}</h1>
<button @click="$i18next.changeLanguage('de')">Deutsch</button>
</template>Svelte Integration
import i18next from 'i18next';
import { createI18nStore } from 'svelte-i18next';
await i18next.init({
lng: 'en',
resources: {
en: { translation: { greeting: 'Hello, {{name}}!' } },
},
});
export const i18n = createI18nStore(i18next);<script>
import { i18n } from './i18n';
</script>
<h1>{$i18n.t('greeting', { name: 'World' })}</h1>Server-Side Rendering
Create a separate i18next instance per request to prevent locale leaking:
import i18next from 'i18next';
export async function createI18nInstance(locale: string) {
const instance = i18next.createInstance();
await instance.init({
lng: locale,
fallbackLng: 'en',
resources: await loadResources(locale),
});
return instance;
}
export async function handleRequest(request: Request) {
const locale = detectLocaleFromRequest(request);
const i18n = await createI18nInstance(locale);
return renderApp(i18n);
}TypeScript Setup
import 'i18next';
import type translation from '../locales/en/translation.json';
declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: 'translation';
resources: {
translation: typeof translation;
};
}
}This enables autocomplete for translation keys and catches missing keys at compile time.
ICU Format Plugin
To use ICU MessageFormat syntax instead of i18next's native format:
npm install i18next-icu intl-messageformatimport i18next from 'i18next';
import ICU from 'i18next-icu';
i18next.use(ICU).init({
lng: 'en',
resources: {
en: {
translation: {
items: '{count, plural, one {# item} other {# items}}',
role: '{gender, select, male {He} female {She} other {They}} is an admin.',
},
},
},
});When to Choose i18next
- Existing projects that need i18n added incrementally
- Applications requiring broad framework support (React, Vue, Svelte, Angular, vanilla)
- Teams that need a large plugin ecosystem (backends, detectors, post-processors)
- Projects using translation management platforms with i18next integration (Locize, Crowdin, Phrase)
- Server-side applications (Express, Fastify, Hono) with Node.js backend
ICU Message Format and Universal i18n Patterns
ICU Message Syntax
ICU MessageFormat is a standard for locale-aware message formatting. Both Paraglide (via plugin) and i18next (via i18next-icu) support it.
Simple Interpolation
Hello, {name}! You have {count} new messages.Plural
Uses CLDR plural categories. Available categories vary by language:
{count, plural,
zero {No items}
one {# item}
two {# items}
few {# items}
many {# items}
other {# items}
}English uses one and other. Arabic uses all six. Polish uses one, few, many, and other. Always include other as the fallback.
Select (Gender / Variants)
{gender, select,
male {He left a comment}
female {She left a comment}
other {They left a comment}
}Nested Plural + Select
{gender, select,
male {{count, plural,
one {He has # notification}
other {He has # notifications}
}}
female {{count, plural,
one {She has # notification}
other {She has # notifications}
}}
other {{count, plural,
one {They have # notification}
other {They have # notifications}
}}
}SelectOrdinal
{rank, selectordinal,
one {#st place}
two {#nd place}
few {#rd place}
other {#th place}
}CLDR Plural Rules by Language
| Language | Categories Used |
|---|---|
| English | one, other |
| French | one, other |
| German | one, other |
| Arabic | zero, one, two, few, many, other |
| Polish | one, few, many, other |
| Russian | one, few, many, other |
| Japanese | other |
| Chinese | other |
Number and Date Formatting
Use the platform Intl API for locale-aware formatting:
Numbers
const formatter = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
});
formatter.format(1234.56);
new Intl.NumberFormat('en-US', { style: 'percent' }).format(0.85);
new Intl.NumberFormat('en-US', {
notation: 'compact',
compactDisplay: 'short',
}).format(1500000);Dates
const date = new Date();
new Intl.DateTimeFormat('en-US', {
dateStyle: 'full',
timeStyle: 'short',
}).format(date);
new Intl.DateTimeFormat('ja-JP', {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);Relative Time
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
rtf.format(-1, 'day');
rtf.format(3, 'hour');List Formatting
const lf = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' });
lf.format(['Alice', 'Bob', 'Charlie']);RTL Layout Support
HTML Direction
Set dir and lang on the <html> element:
<html dir="rtl" lang="ar"></html>Detect direction from locale:
const RTL_LOCALES = new Set(['ar', 'he', 'fa', 'ur', 'ps', 'ku', 'sd', 'yi']);
function getDirection(locale: string): 'ltr' | 'rtl' {
const lang = locale.split('-')[0];
return RTL_LOCALES.has(lang) ? 'rtl' : 'ltr';
}Using Intl.Locale (modern browsers):
function getDirection(locale: string): 'ltr' | 'rtl' {
const { textInfo } = new Intl.Locale(locale);
return textInfo?.direction === 'rtl' ? 'rtl' : 'ltr';
}CSS Logical Properties
Replace physical properties with logical equivalents:
/* Physical (breaks in RTL) */
.card {
margin-left: 16px;
padding-right: 8px;
text-align: left;
border-left: 2px solid blue;
float: left;
}
/* Logical (works in both LTR and RTL) */
.card {
margin-inline-start: 16px;
padding-inline-end: 8px;
text-align: start;
border-inline-start: 2px solid blue;
float: inline-start;
}Property mapping:
| Physical | Logical |
|---|---|
margin-left | margin-inline-start |
margin-right | margin-inline-end |
padding-left | padding-inline-start |
padding-right | padding-inline-end |
border-left | border-inline-start |
border-right | border-inline-end |
left | inset-inline-start |
right | inset-inline-end |
text-align: left | text-align: start |
text-align: right | text-align: end |
width | inline-size |
height | block-size |
RTL-Aware Icons
Icons with directional meaning (arrows, navigation) must be flipped:
[dir='rtl'] .icon-arrow {
transform: scaleX(-1);
}Locale Negotiation and Detection
Strategy Priority Order
1. URL path (/de/about) -- most explicit, SEO-friendly, shareable 2. Cookie (locale=de) -- persists user preference across sessions 3. `Accept-Language` header -- browser preference, good default 4. Geolocation -- least reliable, use as last resort only
Server-Side Detection
function negotiateLocale(
request: Request,
supportedLocales: string[],
defaultLocale: string,
): string {
const cookieLocale = parseCookieLocale(request);
if (cookieLocale && supportedLocales.includes(cookieLocale)) {
return cookieLocale;
}
const acceptHeader = request.headers.get('Accept-Language');
if (acceptHeader) {
const preferred = parseAcceptLanguage(acceptHeader);
for (const lang of preferred) {
const match = supportedLocales.find(
(l) => l === lang || l.startsWith(lang.split('-')[0]),
);
if (match) return match;
}
}
return defaultLocale;
}
function parseAcceptLanguage(header: string): string[] {
return header
.split(',')
.map((part) => {
const [lang, q] = part.trim().split(';q=');
return { lang: lang.trim(), q: q ? parseFloat(q) : 1 };
})
.sort((a, b) => b.q - a.q)
.map(({ lang }) => lang);
}Setting Locale Cookie
function setLocaleCookie(response: Response, locale: string): void {
const maxAge = 60 * 60 * 24 * 365;
response.headers.append(
'Set-Cookie',
`locale=${locale}; Path=/; Max-Age=${maxAge}; SameSite=Lax`,
);
}Hreflang SEO Tags
Every page with multiple locale variants needs hreflang link tags:
<head>
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="de" href="https://example.com/de/about" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr/about" />
<link rel="alternate" hreflang="x-default" href="https://example.com/about" />
</head>Generating Hreflang Tags
type LocaleUrl = { locale: string; url: string };
function generateHreflangTags(
currentPath: string,
supportedLocales: string[],
baseUrl: string,
defaultLocale: string,
): LocaleUrl[] {
const tags: LocaleUrl[] = supportedLocales.map((locale) => ({
locale,
url: `${baseUrl}/${locale}${currentPath}`,
}));
tags.push({
locale: 'x-default',
url: `${baseUrl}/${defaultLocale}${currentPath}`,
});
return tags;
}Sitemap Integration
<url>
<loc>https://example.com/en/about</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about"/>
<xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/about"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/about"/>
</url>Translation Workflow Management
File Organization
locales/
en/
common.json
dashboard.json
settings.json
de/
common.json
dashboard.json
settings.jsonKey Naming Conventions
- Use dot-separated namespaces:
dashboard.chart.title - Group by feature, not by UI component
- Keep keys descriptive:
order_confirmation_email_subjectnotoc_email_sub - Avoid positional keys:
error_messagenoterror_1
Translation Completeness Check
function findMissingKeys(
reference: Record<string, unknown>,
target: Record<string, unknown>,
prefix = '',
): string[] {
const missing: string[] = [];
for (const key of Object.keys(reference)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (!(key in target)) {
missing.push(fullKey);
} else if (
typeof reference[key] === 'object' &&
reference[key] !== null &&
typeof target[key] === 'object' &&
target[key] !== null
) {
missing.push(
...findMissingKeys(
reference[key] as Record<string, unknown>,
target[key] as Record<string, unknown>,
fullKey,
),
);
}
}
return missing;
}Pseudolocalization for Testing
Generate pseudo-translations to catch layout issues before real translations arrive:
function pseudolocalize(text: string): string {
const map: Record<string, string> = {
a: '\u00e0',
e: '\u00e9',
i: '\u00ee',
o: '\u00f6',
u: '\u00fc',
A: '\u00c0',
E: '\u00c9',
I: '\u00ce',
O: '\u00d6',
U: '\u00dc',
};
const converted = text.replace(/[aeiouAEIOU]/g, (c) => map[c] ?? c);
return `[!! ${converted} !!]`;
}Pseudolocalization helps catch:
- Text overflow and truncation
- Hardcoded strings missed by extraction
- Layout issues with longer text (German/Finnish translations are typically 30-40% longer than English)
- RTL layout problems when combined with mirrored pseudo-direction
Paraglide JS
Paraglide JS is a compiler-based i18n library that generates typed, tree-shakable message functions. Only messages used in the application are included in the bundle, resulting in up to 70% smaller i18n payloads compared to runtime libraries.
Installation
npx @inlang/paraglide-js@latest initThe init command creates project.inlang/settings.json, generates message files, detects the bundler, and configures the appropriate plugin.
Project Configuration
{
"baseLocale": "en",
"locales": ["en", "de", "fr", "ar"],
"plugins": [
{
"pluginId": "@inlang/plugin-message-format",
"pathPattern": "./messages/{locale}.json"
}
]
}Message Files
Messages live in messages/{locale}.json:
{
"greeting": "Hello, {name}!",
"item_count": "{count, plural, one {# item} other {# items}}",
"welcome_back": "Welcome back, {name}. You have {count, plural, one {# notification} other {# notifications}}."
}Basic Usage
The compiler generates typed functions in the output directory:
import { m } from './paraglide/messages.js';
import { getLocale, setLocale } from './paraglide/runtime.js';
const message = m.greeting({ name: 'World' });
setLocale('de');
const german = m.greeting({ name: 'Welt' });
const current = getLocale();Bundler Plugins
Vite
import { defineConfig } from 'vite';
import { paraglideVitePlugin } from '@inlang/paraglide-js';
export default defineConfig({
plugins: [
paraglideVitePlugin({
project: './project.inlang',
outdir: './src/paraglide',
strategy: ['cookie', 'localStorage', 'baseLocale'],
}),
],
});Webpack
const { paraglideWebpackPlugin } = require('@inlang/paraglide-js');
module.exports = {
plugins: [
paraglideWebpackPlugin({
project: './project.inlang',
outdir: './src/paraglide',
strategy: ['cookie', 'baseLocale'],
}),
],
};Locale Strategies
The strategy array defines locale resolution order. The compiler tries each strategy in sequence until one returns a locale:
paraglideVitePlugin({
project: './project.inlang',
outdir: './src/paraglide',
strategy: ['cookie', 'globalVariable', 'baseLocale'],
});Built-in strategies: cookie, localStorage, globalVariable, baseLocale, url, preferredLanguage.
Custom Server Strategy
import { defineCustomServerStrategy } from './paraglide/runtime.js';
defineCustomServerStrategy('custom-header', {
getLocale: (request) => {
return request?.headers.get('X-User-Locale') ?? undefined;
},
});
defineCustomServerStrategy('custom-database', {
getLocale: async (request) => {
const userId = extractUserId(request);
if (!userId) return undefined;
const prefs = await getUserPreferences(userId);
return prefs?.locale;
},
});Server-Side Rendering
Use AsyncLocalStorage to scope locale per request and prevent race conditions:
import { overwriteGetLocale, baseLocale } from './paraglide/runtime.js';
import { AsyncLocalStorage } from 'node:async_hooks';
const localeStorage = new AsyncLocalStorage<string>();
overwriteGetLocale(() => {
return localeStorage.getStore() ?? baseLocale;
});
export function onRequest(
request: Request,
next: () => Promise<Response>,
): Promise<Response> {
const locale = detectLocaleFromRequest(request);
return localeStorage.run(locale, () => next());
}SvelteKit Integration
Vite Config
import { sveltekit } from '@sveltejs/kit/vite';
import { paraglideVitePlugin } from '@inlang/paraglide-js';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
sveltekit(),
paraglideVitePlugin({
project: './project.inlang',
outdir: './src/lib/paraglide',
}),
],
});Server Hooks
import type { Handle } from '@sveltejs/kit';
import { paraglideMiddleware } from '$lib/paraglide/server';
const paraglideHandle: Handle = ({ event, resolve }) =>
paraglideMiddleware(
event.request,
({ request: localizedRequest, locale }) => {
event.request = localizedRequest;
return resolve(event, {
transformPageChunk: ({ html }) => html.replace('%lang%', locale),
});
},
);
export const handle: Handle = paraglideHandle;Using Messages in Svelte Components
<script>
import { m } from '$lib/paraglide/messages.js';
import { getLocale } from '$lib/paraglide/runtime.js';
</script>
<h1>{m.greeting({ name: 'World' })}</h1>
<p>{m.item_count({ count: 5 })}</p>
<p>Current locale: {getLocale()}</p>TanStack Start / React Router Integration
Vite Config
import { defineConfig } from 'vite';
import { tanstackStart } from '@tanstack/start/plugin';
import { paraglideVitePlugin } from '@inlang/paraglide-js';
export default defineConfig({
plugins: [
tanstackStart(),
paraglideVitePlugin({
project: './project.inlang',
outdir: './src/paraglide',
strategy: ['cookie', 'baseLocale'],
}),
],
});Root Component with SSR
import {
assertIsLocale,
baseLocale,
isLocale,
overwriteGetLocale,
} from './paraglide/runtime';
import { createContext, useContext } from 'react';
const LocaleContext = createContext(baseLocale);
if (import.meta.env.SSR) {
overwriteGetLocale(() => assertIsLocale(useContext(LocaleContext)));
}
export default function App({
loaderData,
}: {
loaderData: { locale: string };
}) {
return (
<LocaleContext.Provider value={loaderData.locale}>
<Outlet />
</LocaleContext.Provider>
);
}Using Messages in React Components
import { m } from './paraglide/messages.js';
import { getLocale, setLocale } from './paraglide/runtime.js';
function Dashboard() {
return (
<div>
<h1>{m.welcome_back({ name: 'Alice', count: 3 })}</h1>
<button onClick={() => setLocale('de')}>Deutsch</button>
<p>Current: {getLocale()}</p>
</div>
);
}ICU Message Format Plugin
For ICU MessageFormat 1 syntax instead of the default inlang format:
{
"baseLocale": "en",
"locales": ["en", "de"],
"plugins": [
{
"pluginId": "inlang-icu-messageformat-1",
"pathPattern": "./messages/{locale}.json"
}
]
}Tooling
- Sherlock VS Code extension -- inline message previews, click-to-edit translations
- Fink editor -- visual translation editor for non-developer translators
- inlang CLI --
npx @inlang/cli lintto check for missing translations, unused keys
When to Choose Paraglide
- New projects where bundle size and type safety are priorities
- Applications using SvelteKit, TanStack Start, React Router, or Astro
- Teams that want compile-time guarantees for translation completeness
- Projects where tree-shaking unused translations matters (mobile web, performance-critical)