This boilerplate uses typesafe-i18n for type-safe, runtime-safe internationalization. English and Dutch translations are included out of the box.
- Overview
- Built-in Languages
- Language Switcher
- Adding New Languages
- Adding Translation Keys
- Usage in Components
- Configuration
- Best Practices
typesafe-i18n is a fully type-safe internationalization library that:
- Generates TypeScript types from your translations
- Catches missing translations at compile time
- Provides autocomplete for translation keys in your IDE
- Validates translation arguments (e.g.,
{count}variables) - Zero runtime overhead with tree-shaking support
Traditional i18n libraries use string keys that can break silently:
// Traditional i18n (not type-safe)
t('user.greeting', { name: 'John' }); // Typo? Missing key? No error!With typesafe-i18n, you get compile-time safety:
// typesafe-i18n (fully type-safe)
LL.User.greeting({ name: 'John' }); // Autocomplete + type checking!The boilerplate includes two languages:
Located at: apps/web/i18n/en/index.ts
This is the base locale that all other translations reference. All translation keys must exist in the English file.
Located at: apps/web/i18n/nl/index.ts
Dutch translations that mirror the English structure.
The language switcher is located in the navigation bar and allows users to change languages on the fly.
- User clicks the language switcher
- Selected locale is stored in browser localStorage
- UI updates immediately without page reload
- Preference persists across sessions
The language switcher is built into the navigation component:
// Simplified example
import { setLocale } from '@/i18n/i18n-util';
function LanguageSwitcher() {
const changeLanguage = (locale: 'en' | 'nl') => {
setLocale(locale);
// UI updates automatically
};
return (
<select onChange={(e) => changeLanguage(e.target.value)}>
<option value="en">English</option>
<option value="nl">Nederlands</option>
</select>
);
}Create a new file in apps/web/i18n/[locale]/index.ts:
# Example: Adding French
mkdir -p apps/web/i18n/fr
touch apps/web/i18n/fr/index.tsCopy the structure from en/index.ts and translate:
// apps/web/i18n/fr/index.ts
import type { Translation } from "../i18n-types";
const fr = {
Common: {
loading: "Chargement...",
backToHome: "Retour à l'accueil",
back: "Retour",
cancel: "Annuler",
save: "Enregistrer",
// ... all other keys
},
Navigation: {
fullStack: "Full Stack",
profileAndSecurity: "Profil & Sécurité",
// ... all other keys
},
// ... all other sections
} satisfies Translation;
export default fr;Important: You must include all keys from the base English translation. Missing keys will cause TypeScript errors.
Run the type generator:
cd apps/web
pnpm i18n:generateThis command:
- Scans all locale files
- Generates TypeScript types
- Creates type-safe utilities
- Validates all translations
Update the language switcher to include the new locale:
<select onChange={(e) => changeLanguage(e.target.value)}>
<option value="en">English</option>
<option value="nl">Nederlands</option>
<option value="fr">Français</option>
</select>Edit apps/web/i18n/en/index.ts:
const en = {
Common: {
loading: "Loading...",
// Add your new key
newFeature: "New Feature",
},
// Or add a new section
Dashboard: {
title: "Dashboard",
subtitle: "Welcome to your dashboard",
},
} satisfies BaseTranslation;Update all other locale files with translations:
// apps/web/i18n/nl/index.ts
const nl = {
Common: {
loading: "Laden...",
newFeature: "Nieuwe Functie", // Dutch translation
},
Dashboard: {
title: "Dashboard",
subtitle: "Welkom bij je dashboard",
},
} satisfies Translation;cd apps/web
pnpm i18n:generateimport { LL } from '@/i18n/i18n-react';
function MyComponent() {
return (
<div>
<h1>{LL.Dashboard.title()}</h1>
<p>{LL.Dashboard.subtitle()}</p>
</div>
);
}import { LL } from '@/i18n/i18n-react';
function Greeting() {
return <h1>{LL.Home.title()}</h1>;
}Define translation with placeholders:
// apps/web/i18n/en/index.ts
const en = {
User: {
greeting: "Hello, {name:string}!",
itemCount: "You have {count:number} {{item|items}}",
},
}Use with variables:
import { LL } from '@/i18n/i18n-react';
function UserGreeting({ name, itemCount }) {
return (
<div>
<h1>{LL.User.greeting({ name })}</h1>
<p>{LL.User.itemCount({ count: itemCount })}</p>
</div>
);
}typesafe-i18n supports automatic pluralization:
// Translation
itemCount: "{count:number} {{item|items}}"
// Usage
LL.User.itemCount({ count: 1 }) // "1 item"
LL.User.itemCount({ count: 5 }) // "5 items"// Translation
status: "{isActive:boolean|Active|Inactive}"
// Usage
LL.User.status({ isActive: true }) // "Active"
LL.User.status({ isActive: false }) // "Inactive"const en = {
Settings: {
Profile: {
title: "Profile Settings",
subtitle: "Manage your profile",
},
},
}
// Usage
LL.Settings.Profile.title()Located at: apps/web/.typesafe-i18n.json
{
"$schema": "https://unpkg.com/typesafe-i18n@5.27.1/schema/typesafe-i18n.json",
"baseLocale": "en",
"outputPath": "./i18n/",
"outputFormat": "TypeScript",
"generateOnlyTypes": false,
"adapter": "react"
}Key Settings:
baseLocale: "en" - English is the base localeoutputPath: "./i18n/" - Generated types locationadapter: "react" - React-specific utilities
After changing configuration, regenerate types:
cd apps/web
pnpm i18n:generate✅ Good:
LL.Common.loading()❌ Bad:
t('common.loading') // Not type-safe!Group related translations:
const en = {
Auth: {
signIn: "Sign in",
signUp: "Sign up",
signOut: "Sign out",
},
Dashboard: {
title: "Dashboard",
// ...
},
}✅ Good:
Auth: {
emailPlaceholder: "Enter your email",
passwordPlaceholder: "Enter your password",
}❌ Bad:
Auth: {
input1: "Enter your email",
input2: "Enter your password",
}Avoid very long translations in the translation files. For long content (like privacy policies), consider:
- Markdown files
- CMS integration
- Separate content management
When adding new features:
- Add English translation first
- Add translations for all other locales
- Run
pnpm i18n:generateto catch missing keys - Test UI in all languages
✅ Good:
greeting: "Hello, {name:string}!"❌ Bad:
// Don't concatenate strings
const greeting = LL.Common.hello() + ', ' + name + '!';Always use translations, even for labels:
✅ Good:
<button>{LL.Common.save()}</button>❌ Bad:
<button>Save</button>Cause: Types haven't been regenerated.
Solution: Run the type generator:
cd apps/web
pnpm i18n:generateCause: A key exists in English but not in other locales.
Solution: TypeScript will show an error. Add the missing key to all locale files.
Cause: Browser cache or dev server cache.
Solution:
- Hard refresh browser (Ctrl+Shift+R or Cmd+Shift+R)
- Restart dev server
- Clear
.nextcache:rm -rf apps/web/.next
Cause: Types haven't been generated yet.
Solution:
cd apps/web
pnpm i18n:generateFor server components in Next.js:
import { loadLocale } from '@/i18n/i18n-util.sync';
import { i18n } from '@/i18n/i18n-util';
export default function Page({ locale = 'en' }) {
loadLocale(locale);
const LL = i18n()[locale];
return <h1>{LL.Home.title()}</h1>;
}Automatically detect user's browser language:
import { detectLocale } from '@/i18n/i18n-util';
const userLocale = detectLocale(() => {
return navigator.languages;
});
setLocale(userLocale);Define custom formatters in apps/web/i18n/formatters.ts:
export const formatters = {
date: (value: Date) => {
return new Intl.DateTimeFormat('en-US').format(value);
},
currency: (value: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(value);
},
};The mobile app can use the same translation system:
- Copy translation files to
apps/mobile/i18n/ - Install typesafe-i18n for React Native
- Generate types:
pnpm i18n:generate - Use the same
LLutilities
Currently, the mobile app does not have i18n configured. You can add it following the same pattern as the web app.