logo
BETA

Internationalization (i18n)

BUILT-IN

Harpy.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:
    1. URL source (header or query parameter)
    2. Locale cookie (if user previously selected a language)
    3. Accept-Language header (browser preference)
    4. 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:

›_ Terminal
pnpm add @harpy-js/i18n

2. Create i18n configuration:

TypeScript
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:

TypeScript
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:

TypeScript
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

TypeScript
{  "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

TypeScript
{  "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:

TypeScript
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

TypeScript
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

TypeScript
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
TypeScript
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:

TypeScript
// 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:

TypeScript
{  "home": { ... },  "about": { ... },  "products": { ... },  "common": { ... }}

🔑Use Descriptive Keys

Choose clear, descriptive keys that indicate context:

❌ Bad

TypeScript
{  "btn1": "Submit",  "txt1": "Hello"}

✅ Good

TypeScript
{  "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