Internationalization (i18n)
BUILT-INHarpy.js comes with built-in internationalization support, enables you to configure the routing and rendering of content to support multiple languages. Making your site adaptive to different locales includes translated content (localization).
✨ Key Features: Type-safe translations, automatic locale detection, dictionary caching, server and client support, and zero configuration required.
Setting Up i18n
ℹ️ Initialization Check: If you initialized your Harpy.js project with the CLI and selected yes for i18n integration, i18n is already configured! You can skip to Create Dictionary Files. If not, follow the installation steps below.
🍪 Cookies in Harpy.js: The Harpy.js framework engine automatically enables cookies through setupHarpyApp to persist the user's language preference across sessions. Here's how it works:
- Locale Detection Priority: When a request arrives, the I18nInterceptor checks in this order:
- URL source (header or query parameter)
- Locale cookie (if user previously selected a language)
- Accept-Language header (browser preference)
- Default locale (fallback)
- Cookie Setting: When a locale is detected from the URL (not from a cookie), the I18nInterceptor automatically sets a cookie with that locale for 1 year.
- Persistent Experience: On the user's next visit, the cookie is read automatically, so they'll see your site in their previously selected language without needing to specify it again.
- setupHarpyApp: The setupHarpyApp function enables Fastify's cookie parser, which is required to read and write cookies in your Harpy.js application.
Installation (if not already configured)
If you didn't enable i18n during CLI initialization, install and configure it manually:
1. Install the i18n package:
pnpm add @harpy-js/i18n
2. Create i18n configuration:
1// src/i18n/i18n.config.ts2import { I18nModuleOptions } from '@harpy-js/i18n';34export const i18nConfig: I18nModuleOptions = {5 defaultLocale: 'en', // Default language6 locales: ['en', 'fr'], // Supported locales7 urlPattern: 'header', // 'query' or 'header' (header for cleaner URLs)8 translationsPath: '../dictionaries',9 cookieName: 'locale', // Cookie to persist user language choice10 queryParam: 'lang', // Query parameter (if using 'query' pattern)11};💡 URL Pattern Explanation:
- header: Locale from Accept-Language header (cleaner URLs)
- query: Locale from query param like ?lang=fr
3. Create dictionary loader:
1// src/i18n/get-dictionary.ts2// Enumerate all dictionaries for type support3const dictionaries = {4 en: () =>5 import('../dictionaries/en.json', { with: { type: 'json' } }).then(6 (module) => module.default,7 ),8 fr: () =>9 import('../dictionaries/fr.json', { with: { type: 'json' } }).then(10 (module) => module.default,11 ),12};1314// Automatically infer Dictionary type from English15export type Dictionary = Awaited<ReturnType<typeof dictionaries.en>>;1617// In-memory cache for performance18const dictionaryCache = new Map<string, Dictionary>();1920// Load with caching (dictionary loaded once, reused)21export const getDictionary = async (locale: string): Promise<Dictionary> => {22 if (dictionaryCache.has(locale)) {23 return dictionaryCache.get(locale)!;24 }2526 const dict = await (dictionaries[locale as keyof typeof dictionaries]?.() ?? 27 dictionaries.en());28 29 dictionaryCache.set(locale, dict);30 return dict;31};4. Import I18nModule in your app module:
1// src/app.module.ts2import { Module } from '@nestjs/common';3import { I18nModule } from '@harpy-js/i18n';4import { i18nConfig } from './i18n/i18n.config';5import { HomeModule } from './features/home/home.module';67@Module({8 imports: [9 I18nModule.forRoot(i18nConfig),10 HomeModule,11 ],12})13export class AppModule {}Creating Dictionary Files
Create JSON files for each language in the src/dictionaries/ directory:
src/dictionaries/en.json
{ "welcome": "Welcome", "home": "Home", "about": "About", "hero": { "title": "Welcome to Harpy", "subtitle": "A powerful NestJS + React framework", "description": "Built for speed, precision, and adaptability" }, "features": { "title": "Why Choose Harpy?", "lightning": { "title": "Lightning Fast", "description": "Optimized SSR with automatic hydration" } }, "demo": { "title": "Try It Out", "counter": "Counter", "clicks": "clicks" }}src/dictionaries/fr.json
{ "welcome": "Bienvenue", "home": "Accueil", "about": "À propos", "hero": { "title": "Bienvenue sur Harpy", "subtitle": "Un puissant framework NestJS + React", "description": "Conçu pour la vitesse, la précision et l'adaptabilité" }, "features": { "title": "Pourquoi choisir Harpy?", "lightning": { "title": "Ultra Rapide", "description": "SSR optimisé avec hydratation automatique" } }, "demo": { "title": "Essayez-le", "counter": "Compteur", "clicks": "clics" }}Server-Side Translations
Load translations in your controllers and pass them to your views. Harpy.js handles the rest automatically.
In Your Controller
Use the @CurrentLocale() decorator to inject the current locale:
1// src/features/home/home.controller.ts2import { Controller, Get } from '@nestjs/common';3import { JsxRender, type PageProps } from '@harpy-js/core';4import { CurrentLocale } from '@harpy-js/i18n';5import HomePage from './views/homepage';6import { getDictionary } from '../../i18n/get-dictionary';78@Controller()9export class HomeController {10 @Get()11 @JsxRender(HomePage, {12 meta: {13 title: 'Home Page',14 description: 'Welcome to our multilingual app',15 keywords: 'home, welcome, i18n, multilingual',16 },17 })18 async home(@CurrentLocale() locale: string): Promise<PageProps> {19 // Locale is automatically injected from request20 const dict = await getDictionary(locale);21 22 return {23 dict,24 locale,25 };26 }27}🎯 How it Works: The I18nInterceptor automatically detects the locale from your configured URL pattern (header or query) and stores it in the request. The @CurrentLocale() decorator retrieves it for you.
In Your View Component
1// src/features/home/views/homepage.tsx2import { type PageProps as CorePageProps } from '@harpy-js/core';3import { Dictionary } from '../../i18n/get-dictionary';45export interface PageProps extends CorePageProps {6 dict: Dictionary;7 locale: string;8}910export default function HomePage({ dict, locale }: PageProps) {11 return (12 <div>13 <h1>{dict.hero.title}</h1>14 <p>{dict.hero.subtitle}</p>15 <p>{dict.hero.description}</p>16 17 <section>18 <h2>{dict.features.title}</h2>19 <div>20 <h3>{dict.features.lightning.title}</h3>21 <p>{dict.features.lightning.description}</p>22 </div>23 </section>24 </div>25 );26}✅ Type Safety: The Dictionary type is automatically inferred from your English dictionary, providing autocomplete and type checking for all translation keys! Always extend CorePageProps from @harpy-js/core.
Client-Side Translations
Client components receive translations via props passed from the server. Since Harpy.js uses server-side rendering, all translations are available from the start with full type safety.
Interactive Component Example
1// src/components/Counter.tsx2'use client';34import { useState } from 'react';5import { Dictionary } from '../i18n/get-dictionary';67interface CounterProps {8 dict: Dictionary;9 locale: string;10}1112export function Counter({ dict, locale }: CounterProps) {13 const [count, setCount] = useState(0);1415 return (16 <div className="p-4 border rounded">17 <h3>{dict.demo.counter}</h3>18 <p className="text-lg font-bold">{count} {dict.demo.clicks}</p>19 <button 20 onClick={() => setCount(count + 1)}21 className="px-4 py-2 bg-blue-600 text-white rounded"22 >23 Increment24 </button>25 </div>26 );27}Language Switcher
Create a language switcher component using the useI18n() hook from @harpy-js/core/client:
🎣 useI18n Hook: The useI18n hook provides the switchLocale() function that correctly integrates with your i18n configuration.
- The hook automatically detects your configured URL pattern (header or query)
- It communicates with the I18nInterceptor to trigger locale detection
- It works seamlessly with your cookie persistence system
- When called, it triggers a page reload to fetch the correct dictionary for the new locale
1'use client';23import { useI18n } from '@harpy-js/core/client';4import { useState } from 'react';56/**7 * Language Switcher Component8 *9 * Uses the useI18n hook to switch locales.10 * The hook automatically detects whether to use query params or header mode.11 */12export function LanguageSwitcher() {13 const { switchLocale } = useI18n();14 const [isLoading, setIsLoading] = useState(false);1516 const handleSwitchLocale = (locale: string) => {17 setIsLoading(true);18 (switchLocale(locale) as Promise<void>)19 .then(() => {20 // Page will reload, so this won't execute21 })22 .catch((error: unknown) => {23 console.error('Failed to switch locale:', error);24 setIsLoading(false);25 });26 };2728 return (29 <div className="flex gap-2">30 <button31 type="button"32 onClick={() => {33 handleSwitchLocale('en');34 }}35 disabled={isLoading}36 className="px-3 py-1 rounded bg-amber-600 hover:bg-amber-700 disabled:bg-amber-800 text-white font-medium transition-colors"37 >38 EN39 </button>40 <button41 type="button"42 onClick={() => {43 handleSwitchLocale('fr');44 }}45 disabled={isLoading}46 className="px-3 py-1 rounded bg-amber-600 hover:bg-amber-700 disabled:bg-amber-800 text-white font-medium transition-colors"47 >48 FR49 </button>50 </div>51 );52}💡 Tip: Always set a default locale in your I18nModuleOptions configuration to ensure a consistent experience for users without a locale cookie or URL parameter.
Advanced Features
Nested Translations
Organize your translations into nested objects for better structure and maintainability:
// src/dictionaries/en.json{ "pages": { "home": { "title": "Home Page", "meta": { "description": "Welcome to our homepage" } }, "about": { "title": "About Us", "team": { "title": "Our Team", "members": { "ceo": "Chief Executive Officer", "cto": "Chief Technology Officer" } } } }, "common": { "buttons": { "submit": "Submit", "cancel": "Cancel", "save": "Save" }, "errors": { "required": "This field is required", "invalid": "Invalid input" } }}// Access nested values with dot notation:<h1>{dict.pages.about.team.title}</h1><button>{dict.common.buttons.submit}</button><span>{dict.common.errors.required}</span>Best Practices
📁Organize by Feature
Structure your dictionaries by feature or page for better maintainability:
{ "home": { ... }, "about": { ... }, "products": { ... }, "common": { ... }}🔑Use Descriptive Keys
Choose clear, descriptive keys that indicate context:
❌ Bad
{ "btn1": "Submit", "txt1": "Hello"}✅ Good
{ "submitButton": "Submit", "welcomeMessage": "Hello"}🌍Keep English as Source of Truth
Always use English (en.json) as your base dictionary. The TypeScript types are derived from it, ensuring all other languages have the same structure.
🚀Leverage Caching
Dictionaries are cached in memory after first load. Don't worry about calling getDictionary multiple times - it's optimized!
🎉 That's It!
Internationalization in Harpy.js is simple, powerful, and built right into the framework. No external dependencies, no complex setup, just straightforward multilingual support.
- ✅Built-in feature, not an afterthought
- ✅Type-safe translations with autocompletion
- ✅Server and client support
- ✅Automatic caching for performance
- ✅Zero configuration required