Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
YouTube Security VideosOpenAI: “It Blew Me Away” | Box’s First Look at GPT-6 Astra(21.09.2026 um 18:13 Uhr)
YouTube Security VideosGoogle Ads: Win the holiday season in 90 seconds | Rethink Retail 2026(21.09.2026 um 18:13 Uhr)
YouTube Security VideosPC-WELT: 5 crazy Sitzpositionen, die jeder Gamer kennt 😀(21.09.2026 um 18:52 Uhr)
Videos & KonferenzenMariaDB Foundation: The Fork Series #1 - The Future of Databases(21.09.2026 um 19:18 Uhr)
Videos & KonferenzenPC-WELT: 5 crazy Sitzpositionen, die jeder Gamer kennt 😀(21.09.2026 um 18:52 Uhr)
Unix & Linux ServerSecurity: Denial of Service in Memcached (Ubuntu)(21.09.2026 um 19:21 Uhr)
YouTube Security VideosOpenAI: “It Blew Me Away” | Box’s First Look at GPT-6 Astra(21.09.2026 um 18:13 Uhr)
YouTube Security VideosGoogle Ads: Win the holiday season in 90 seconds | Rethink Retail 2026(21.09.2026 um 18:13 Uhr)
YouTube Security VideosPC-WELT: 5 crazy Sitzpositionen, die jeder Gamer kennt 😀(21.09.2026 um 18:52 Uhr)
Videos & KonferenzenMariaDB Foundation: The Fork Series #1 - The Future of Databases(21.09.2026 um 19:18 Uhr)
Videos & KonferenzenPC-WELT: 5 crazy Sitzpositionen, die jeder Gamer kennt 😀(21.09.2026 um 18:52 Uhr)
Unix & Linux ServerSecurity: Denial of Service in Memcached (Ubuntu)(21.09.2026 um 19:21 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Building Multilingual APIs with @hazeljs/i18n

I just shipped internationalization support for HazelJS — a TypeScript-first Node.js framework I've been building. The new @hazeljs/i18n package gives you locale detection, JSON-based translations, pluralization via the native Intl API, a…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

I just shipped internationalization support for HazelJS — a TypeScript-first Node.js framework I've been building. The new @hazeljs/i18n package gives you locale detection, JSON-based translations, pluralization via the native Intl API, and currency/date formatting — all with zero external dependencies and the same decorator-driven API you already know from the rest of the framework.









Why build i18n from scratch?



Most i18n libraries for Node.js are either too heavy (pulling in ICU data bundles and parser machinery you may not need) or too loosely coupled to your framework (requiring you to wire up middleware, guards, and request context yourself). I wanted something that:





  • Fits the HazelJS module system — register once in your root module with forRoot(), done.


  • Uses only what Node.js shipsfs/promises for loading JSON files and the native Intl API for formatting. No polyfills, no ICU bundles.


  • Feels familiar@Lang() is just another parameter decorator, sitting alongside @Param(), @Query(), and @Body() in your controller signatures.


  • Handles the hard parts — plural rules for Arabic, German, French (and the 200+ other CLDR locales) via Intl.PluralRules, without a single extra package.









Features





  • I18nModule.forRoot() / forRootAsync() — loads all <locale>.json files at startup, wires the service and middleware into the DI container


  • LocaleMiddleware — detects locale from query param (?lang=fr), cookie, or Accept-Language header in configurable priority order


  • @Lang() parameter decorator — injects the request-scoped locale string directly into controller methods


  • I18nService.t() — dot-notation key lookup, {placeholder} interpolation, and Intl.PluralRules-backed pluralization with transparent fallback locale


  • I18nService.format.*number(), date(), currency(), relative() via native Intl.* APIs


  • I18nInterceptor — optional: auto-translates a message field in response objects using the request locale


  • Graceful degradation — missing translation directories and malformed JSON files are logged and skipped; the service starts cleanly without translations rather than crashing









Installation






npm install @hazeljs/i18n






npm: @hazeljs/i18n









Quick start






1. Create translation files



Put locale JSON files anywhere — ./translations/ by default:




// translations/en.json
{
"welcome": "Welcome, {name}!",
"products": {
"count": { "one": "{count} product", "other": "{count} products" },
"notFound": "Product \"{id}\" was not found."
}
}









// translations/fr.json
{
"welcome": "Bienvenue, {name} !",
"products": {
"count": { "one": "{count} produit", "other": "{count} produits" },
"notFound": "Le produit « {id} » est introuvable."
}
}









2. Register the module






import { HazelModule } from '@hazeljs/core';
import { I18nModule } from '@hazeljs/i18n';

@HazelModule({
imports: [
I18nModule.forRoot({
defaultLocale: 'en',
fallbackLocale: 'en',
translationsPath: './translations',
detection: ['query', 'cookie', 'header'],
}),
],
})
export class AppModule {}









3. Apply the locale middleware globally






// main.ts
const app = new HazelApp(AppModule);
const localeMw = app.getContainer().resolve(LocaleMiddleware);
app.use((req, res, next) => localeMw.handle(req, res, next));
await app.listen(3000);









4. Translate in a controller






import { Controller, Get } from '@hazeljs/core';
import { I18nService, Lang } from '@hazeljs/i18n';

@Controller('/greet')
export class GreetController {
constructor(private readonly i18n: I18nService) {}

@Get('/')
hello(@Lang() locale: string) {
return {
message: this.i18n.t('welcome', { locale, vars: { name: 'Alice' } }),
};
// GET /greet?lang=fr → { message: "Bienvenue, Alice !" }
// GET /greet → { message: "Welcome, Alice!" }
}
}












Real-world example: multilingual product catalog



I built a complete starter — hazeljs-i18n-starter — to show what this looks like in practice. It's a product catalog REST API with 5 seeded products, CRUD endpoints, category browsing, and pagination. Every response field — labels, prices, dates, counts, error messages — is locale-aware.



Here's what makes it interesting:






Locale-aware pricing



Different locales get different currencies and number formats. The service layer picks the right currency for the region and uses format.currency() to format it:




// src/products/products.service.ts
const LOCALE_CURRENCY: Record<string, string> = {
fr: 'EUR',
de: 'EUR',
ar: 'SAR',
en: 'USD',
};

private localize(product: Product, locale: string): LocalizedProduct {
const currency = LOCALE_CURRENCY[locale] ?? 'USD';

return {
...product,
// $299.99, 299,99 €, ٢٩٩٫٩٩ ر.س. — all from one call
price: this.i18n.format.currency(product.price, locale, currency),
// "Mar 4, 2026, 12:00 PM" vs "4 mars 2026 à 12:00"
createdAt: this.i18n.format.date(product.createdAt, locale, {
dateStyle: 'medium',
timeStyle: 'short',
}),
category: this.i18n.t(`categories.${product.category}`, { locale }),
stockLabel: this.buildStockLabel(product.stock, locale),
};
}






The format.currency() call handles everything: symbol position, decimal separator, thousands separator — all from the native Intl.NumberFormat. No conversion tables needed.






Pluralization that actually works across languages



English has two plural forms. Arabic has six. The package handles both transparently via Intl.PluralRules:




// translations/ar.json
{
"products": {
"count": {
"zero": "لم يتم العثور على منتجات.",
"one": "تم العثور على {count} منتج.",
"two": "تم العثور على {count} منتجَين.",
"few": "تم العثور على {count} منتجات.",
"many": "تم العثور على {count} منتجاً.",
"other": "تم العثور على {count} منتج."
}
}
}









this.i18n.t('products.count', { locale: 'ar', count: 2, vars: { count: '2' } })
// → "تم العثور على 2 منتجَين." (Arabic dual form — "two")

this.i18n.t('products.count', { locale: 'ar', count: 11, vars: { count: '11' } })
// → "تم العثور على 11 منتجاً." (Arabic "many" form)

this.i18n.t('products.count', { locale: 'en', count: 11, vars: { count: '11' } })
// → "11 products found." (English "other" form)






The same t() call works for all locales — you just write the plural forms in the JSON files for the languages that need them and omit the ones that don't.






Async configuration from environment variables



The starter uses forRootAsync() so locale settings come from the environment, not hardcoded values:




// src/app.module.ts
I18nModule.forRootAsync({
useFactory: (config: ConfigService) => ({
defaultLocale: config.get<string>('DEFAULT_LOCALE') ?? 'en',
fallbackLocale: config.get<string>('FALLBACK_LOCALE') ?? 'en',
translationsPath: config.get<string>('TRANSLATIONS_PATH') ?? './translations',
detection: ['query', 'cookie', 'header'],
}),
inject: [ConfigService],
})






Swap DEFAULT_LOCALE=fr in your staging environment and every undetected request defaults to French — no redeploy required.






Localized error responses



A custom exception filter reads the locale from the request (set by LocaleMiddleware earlier in the pipeline) and translates error messages before sending them to the client:




// src/common/i18n-exception.filter.ts
@Catch(HttpError)
export class I18nExceptionFilter implements ExceptionFilter<HttpError> {
constructor(private readonly i18n: I18nService) {}

catch(exception: HttpError, host: ArgumentsHost): void {
const locale = getLocaleFromRequest(host.switchToHttp().getRequest()) ?? 'en';
const rawMessage = exception.message ?? 'Internal server error';

// Translate if the message is an i18n key; otherwise use as-is
const message = this.i18n.has(rawMessage, locale)
? this.i18n.t(rawMessage, { locale })
: rawMessage;

host.switchToHttp().getResponse().status(exception.statusCode ?? 500).json({
statusCode: exception.statusCode ?? 500,
message,
locale,
});
}
}






A 404 for a missing product now looks like this depending on the request locale:




GET /products/999?lang=fr
# → { "statusCode": 404, "message": "Aucun produit avec l'identifiant « 999 » n'a été trouvé.", "locale": "fr" }

GET /products/999?lang=de
# → { "statusCode": 404, "message": "Produkt mit der ID \"999\" wurde nicht gefunden.", "locale": "de" }












The full request flow



Here's how a request travels through the i18n layer:




Incoming request
→ LocaleMiddleware (detects locale from query / cookie / Accept-Language)
→ sets __hazel_locale__ on the request object
→ sets Content-Language response header
→ Controller method
→ @Lang() injects the locale string
→ I18nService.t() resolves the key, applies interpolation + pluralization
→ I18nService.format.currency() / .date() formats the output
→ Response






Nothing magical — just a middleware writing a value to the request object that a decorator reads later. Simple to debug, easy to test.









Trying the starter






git clone https://github.com/hazel-js/hazeljs-i18n-starter
cd hazeljs-i18n-starter
cp .env.example .env
npm install
npm run start:dev






Then try a few requests:




# English — USD prices
curl http://localhost:3000/products

# French — EUR prices, French dates
curl "http://localhost:3000/products?lang=fr"

# German — EUR prices, German number format
curl "http://localhost:3000/products?lang=de"

# Arabic — SAR prices, Arabic plural forms, RTL text
curl "http://localhost:3000/products?lang=ar"

# Localized 404
curl "http://localhost:3000/products/999?lang=fr"






You can also send Accept-Language: de as a header or set Cookie: locale=ar — the middleware picks up whichever comes first in the configured detection order.









Adding a new locale




  1. Create translations/<locale>.json using the same key structure as en.json.

  2. Restart the server.



That's it. TranslationLoader reads all *.json files from the directory at startup. No code changes, no imports, no registration. The new locale appears in i18n.getLocales() automatically.









What's next



A few things I'm looking at for the next iteration:





  • Namespaced loaders — lazy-load translation namespaces on demand for large apps instead of loading everything upfront


  • @I18nKey() DTO decorator — mark response DTO fields as translation keys so the interceptor can translate entire objects automatically


  • Hot reload in development — watch the translations directory and reload without restarting









Learn more



Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Building Multilingual APIs with @hazeljs/i18n

Thematisch verwandte Begriffe: Building, Multilingual, APIs, with · 6 Treffer

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-82412 | ntopng is a web-based network traffic monitoring application. Prior to 6…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick