Internationalization (i18n)
BUILT-INHarpy.js comes with built-in internationalization support, unlike many frameworks that require third-party libraries. Build multilingual applications with ease using our dictionary-based translation system.
🌍 Why Built-in i18n Matters: Many modern frameworks overlook internationalization, forcing developers to integrate complex third-party solutions. Harpy.js includes i18n as a core feature, providing seamless multilingual support out of the box.
✨ Key Features: Type-safe translations, automatic locale detection, dictionary caching, server and client support, and zero configuration required.
Why Harpy.js Has Built-in i18n
Unlike popular frameworks such as Next.js (which only added i18n routing in v10+) or Create React App (which requires react-i18next or similar), Harpy.js includes internationalization as a fundamental feature from day one.
❌Other Frameworks
- • Require external libraries (react-intl, i18next)
- • Complex setup and configuration
- • Multiple competing solutions
- • Additional bundle size
- • Inconsistent patterns across projects
✅Harpy.js
- • Built into the core framework
- • Zero configuration needed
- • Consistent approach across all projects
- • Optimized and lightweight
- • Type-safe by design
Setting Up i18n
i18n is already configured in your Harpy.js project! Just add your translation dictionaries and start using them.
1. Create 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"
}
}2. Configure Dictionary Loader
Update src/i18n/get-dictionary.ts to include your languages:
const dictionaries = {
en: () =>
import('../dictionaries/en.json', { with: { type: 'json' } }).then(
(module) => module.default,
),
fr: () =>
import('../dictionaries/fr.json', { with: { type: 'json' } }).then(
(module) => module.default,
),
es: () =>
import('../dictionaries/es.json', { with: { type: 'json' } }).then(
(module) => module.default,
),
};
export type Dictionary = Awaited<ReturnType<typeof dictionaries.en>>;
const dictionaryCache = new Map<string, Dictionary>();
export const getDictionary = async (locale: string): Promise<Dictionary> => {
if (dictionaryCache.has(locale)) {
return dictionaryCache.get(locale)!;
}
const dict = await (dictionaries[locale as keyof typeof dictionaries]?.() ??
dictionaries.en());
dictionaryCache.set(locale, dict);
return dict;
};💡 Performance Tip: Dictionaries are automatically cached in memory after first load, ensuring fast translation lookups on subsequent requests.
Server-Side Translations
Load translations in your controllers and pass them to your views. Harpy.js handles the rest automatically.
In Your Controller
import { Controller, Get } from '@nestjs/common';
import { JsxRender } from '@hepta-solutions/harpy-core';
import HomePage from './views/homepage';
import { getDictionary } from '../i18n/get-dictionary';
@Controller()
export class HomeController {
@Get()
@JsxRender(HomePage)
async home() {
// Load dictionary for current locale
const dict = await getDictionary('en');
return {
dict,
locale: 'en',
};
}
@Get(':locale')
@JsxRender(HomePage)
async homeWithLocale(@Param('locale') locale: string) {
// Dynamic locale from URL
const dict = await getDictionary(locale);
return {
dict,
locale,
};
}
}In Your View Component
import { Dictionary } from '../i18n/get-dictionary';
export interface PageProps {
dict: Dictionary;
locale: string;
}
export default function HomePage({ dict, locale }: PageProps) {
return (
<div>
<h1>{dict.hero.title}</h1>
<p>{dict.hero.subtitle}</p>
<p>{dict.hero.description}</p>
<section>
<h2>{dict.features.title}</h2>
<div>
<h3>{dict.features.lightning.title}</h3>
<p>{dict.features.lightning.description}</p>
</div>
</section>
</div>
);
}✅ Type Safety: The Dictionary type is automatically inferred from your English dictionary, providing autocomplete and type checking for all translation keys!
Client-Side Translations
For interactive components marked with 'use client', use the useI18n hook to access translations and switch locales.
Creating a Language Switcher
'use client';
import { useI18n } from '@hepta-solutions/harpy-core/client';
export function LanguageSwitcher() {
const { locale, switchLocale, t } = useI18n();
return (
<div className="flex gap-2">
<button
onClick={() => switchLocale('en')}
className={`px-3 py-1 rounded ${
locale === 'en'
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700'
}`}
>
English
</button>
<button
onClick={() => switchLocale('fr')}
className={`px-3 py-1 rounded ${
locale === 'fr'
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700'
}`}
>
Français
</button>
</div>
);
}Using Translations in Client Components
'use client';
import { useI18n } from '@hepta-solutions/harpy-core/client';
import { useState } from 'react';
export function Counter() {
const { t } = useI18n();
const [count, setCount] = useState(0);
return (
<div>
<h3>{t('demo.counter')}</h3>
<p>{count} {t('demo.clicks')}</p>
<button onClick={() => setCount(count + 1)}>
{t('demo.increment')}
</button>
</div>
);
}⚠️ Important: When switching locales on the client, the page will reload to fetch the new dictionary from the server. This ensures SSR consistency and optimal performance.
Advanced Features
Locale Detection
You can detect the user's preferred locale from various sources:
import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';
@Controller()
export class HomeController {
@Get()
@JsxRender(HomePage)
async home(@Req() req: Request) {
// 1. From URL parameter
const localeFromUrl = req.params.locale;
// 2. From cookie
const localeFromCookie = req.cookies?.locale;
// 3. From Accept-Language header
const localeFromHeader = req.headers['accept-language']?.split(',')[0]?.split('-')[0];
// 4. Fallback to default
const locale = localeFromUrl ||
localeFromCookie ||
localeFromHeader ||
'en';
const dict = await getDictionary(locale);
return { dict, locale };
}
}Nested Translations
Organize your translations in a nested structure for better maintainability:
{
"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>Dynamic Values in Translations
Use template strings for dynamic content:
// Dictionary
{
"greeting": "Hello, {{name}}!",
"itemsCount": "You have {{count}} items in your cart"
}
// Usage in component
export function Greeting({ dict, userName }: Props) {
const greeting = dict.greeting.replace('{{name}}', userName);
return <h1>{greeting}</h1>;
}
// Or create a helper function
function t(key: string, values: Record<string, string>) {
return key.replace(/{{(\w+)}}/g, (_, k) => values[k] || '');
}
<p>{t(dict.itemsCount, { count: '5' })}</p>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!
✏️Validate Translations
Create a script to ensure all languages have the same keys:
// scripts/validate-translations.ts
const en = require('../src/dictionaries/en.json');
const fr = require('../src/dictionaries/fr.json');
function getKeys(obj: any, prefix = ''): string[] {
return Object.keys(obj).flatMap(key => {
const path = prefix ? `${prefix}.${key}` : key;
return typeof obj[key] === 'object'
? getKeys(obj[key], path)
: [path];
});
}
const enKeys = getKeys(en).sort();
const frKeys = getKeys(fr).sort();
const missing = enKeys.filter(k => !frKeys.includes(k));
if (missing.length) {
console.error('Missing FR translations:', missing);
process.exit(1);
}Migration from Other i18n Libraries
Coming from react-i18next, react-intl, or next-i18next? Here's how Harpy.js i18n compares:
| Feature | Other Libraries | Harpy.js |
|---|---|---|
| Setup | Requires provider, config files | Zero config, built-in |
| Bundle Size | 10-50KB extra | 0KB (built-in) |
| Type Safety | Manual types or none | Automatic from dictionaries |
| SSR Support | Complex setup required | Native SSR support |
| Performance | Runtime overhead | Cached & 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