Guides
Internationalization (i18n)
Guide to using Paraglide JS for internationalization in Roxabi Boilerplate
Overview
Roxabi Boilerplate uses Paraglide JS for internationalization. Paraglide compiles translations into tree-shakeable JavaScript functions, resulting in up to 70% smaller i18n bundle sizes compared to traditional libraries.
Key Benefits
- Tree-shakeable: Only used translations are bundled
- Type-safe: Full TypeScript support with autocomplete
- Fast: No runtime parsing, messages are pre-compiled
- Simple: No complex configuration or context providers
Project Structure
apps/web/
├── messages/ # Source translation files
│ ├── en.json # English (base locale)
│ └── fr.json # French
├── project.inlang/ # Paraglide configuration
│ └── settings.json
└── src/paraglide/ # Auto-generated (do not edit)
├── messages.js # Message functions
└── runtime.js # Locale utilitiesConfiguration
The Paraglide Vite plugin is configured in apps/web/vite.config.ts:
paraglideVitePlugin({
project: './project.inlang',
outdir: './src/paraglide',
strategy: ['cookie', 'preferredLanguage', 'url', 'baseLocale'],
})Locale settings are in apps/web/project.inlang/settings.json:
{
"baseLocale": "en",
"locales": ["en", "fr"],
"plugin.inlang.messageFormat": {
"pathPattern": "./messages/{locale}.json"
}
}Adding Translations
1. Add Messages to JSON Files
Add your message to each locale file in apps/web/messages/:
// messages/en.json
{
"greeting": "Hello, {name}!",
"items_count": "{count, plural, one {# item} other {# items}}"
}// messages/fr.json
{
"greeting": "Bonjour, {name}!",
"items_count": "{count, plural, one {# article} other {# articles}}"
}2. Use Messages in Components
Import and call message functions:
import { m } from '@/paraglide/messages'
function Welcome() {
return (
<div>
<h1>{m.greeting({ name: 'World' })}</h1>
<p>{m.items_count({ count: 5 })}</p>
</div>
)
}Locale Management
Getting the Current Locale
import { getLocale } from '@/paraglide/runtime'
const currentLocale = getLocale() // "en" or "fr"Changing the Locale
import { setLocale } from '@/paraglide/runtime'
setLocale('fr') // Switch to FrenchAvailable Locales
import { locales, baseLocale } from '@/paraglide/runtime'
console.log(locales) // ["en", "fr"]
console.log(baseLocale) // "en"Locale Switcher Component
A ready-to-use locale switcher is available at apps/web/src/components/LocaleSwitcher.tsx:
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@repo/ui'
import { Languages } from 'lucide-react'
import { m } from '@/paraglide/messages'
import { getLocale, locales, setLocale } from '@/paraglide/runtime'
const localeLabels: Record<string, string> = {
en: 'English',
fr: 'Français',
}
export function LocaleSwitcher() {
const currentLocale = getLocale()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" aria-label={m.language_label()}>
<Languages className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{locales.map((locale) => (
<DropdownMenuItem
key={locale}
onClick={() => setLocale(locale)}
className={locale === currentLocale ? 'bg-accent' : ''}
>
{localeLabels[locale] ?? locale.toUpperCase()}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}Adding a New Locale
- Add the locale to
apps/web/project.inlang/settings.json:
{
"locales": ["en", "fr", "es"]
}- Create the translation file
apps/web/messages/es.json:
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"greeting": "¡Hola, {name}!"
}- Run the dev server to regenerate the paraglide output:
bun devMessage Syntax
Paraglide uses the ICU Message Format:
Variables
{
"welcome": "Welcome, {username}!"
}Pluralization
{
"notifications": "{count, plural, =0 {No notifications} one {# notification} other {# notifications}}"
}Select (Gender, etc.)
{
"invite": "{gender, select, male {He invited you} female {She invited you} other {They invited you}}"
}