
Research
/Security News
Miasma Mini Shai-Hulud Hits ImmobiliareLabs npm Packages
Miasma Mini Shai-Hulud hits @immobiliarelabs Backstage plugins, targeting GitLab and LDAP auth packages on npm.
Internationalization (i18n) plugin for the Najm framework with automatic language detection, translation management, and decorator-based injection.
{{variable}} syntaxbun add najm-i18n
import { Server, Controller, Get } from 'najm-core';
import { i18n, I18n, type TFn } from 'najm-i18n';
// Define translations
const translations = {
en: {
hello: 'Hello',
welcome: 'Welcome {{name}}!',
errors: {
notFound: 'Not found',
},
},
fr: {
hello: 'Bonjour',
welcome: 'Bienvenue {{name}}!',
errors: {
notFound: 'Non trouvé',
},
},
};
@Controller('/api')
class ApiController {
@I18n()
t!: TFn;
@Get('/greet')
greet() {
return { message: this.t('hello') };
}
@Get('/welcome/:name')
welcome(@Params('name') name: string) {
return { message: this.t('welcome', { name }) };
}
}
// Initialize server with i18n plugin
new Server()
.use(i18n({
translations,
defaultLanguage: 'en',
supportedLanguages: ['en', 'fr'],
}))
.load(ApiController)
.listen(3000);
interface I18nOptions {
// Translation data (key-value pairs per language)
translations: Record<string, Record<string, any>>;
// Default language to use when detection fails
defaultLanguage?: string; // Default: 'en'
// List of supported languages
supportedLanguages?: string[]; // Default: ['en']
// Language detection order
order?: ('querystring' | 'header' | 'cookie' | 'path')[]; // Default: ['cookie', 'querystring', 'header']
// Query string parameter name
lookupQueryString?: string; // Default: 'lang'
// Cookie name for language storage
lookupCookie?: string; // Default: 'language'
// Header key for language detection
lookupFromHeaderKey?: string; // Default: 'language'
// URL path index for language detection
lookupFromPathIndex?: number; // Default: 0
// Cache detected language in cookie
caches?: ('cookie')[]; // Default: ['cookie']
// Cookie configuration
cookieOptions?: {
path?: string;
domain?: string;
sameSite?: 'Strict' | 'Lax' | 'None';
secure?: boolean;
maxAge?: number;
httpOnly?: boolean;
};
// Case-insensitive language matching
ignoreCase?: boolean; // Default: true
// Convert detected language code
convertDetectedLanguage?: (lang: string) => string;
// Enable debug logging
debug?: boolean; // Default: false
}
new Server()
.use(i18n({
translations: {
en: { hello: 'Hello' },
fr: { hello: 'Bonjour' },
},
defaultLanguage: 'en',
supportedLanguages: ['en', 'fr'],
}))
.load(/* controllers */)
.listen(3000);
new Server()
.use(i18n({
translations: {
en: require('./locales/en.json'),
fr: require('./locales/fr.json'),
es: require('./locales/es.json'),
de: require('./locales/de.json'),
},
defaultLanguage: 'en',
supportedLanguages: ['en', 'fr', 'es', 'de'],
order: ['cookie', 'header', 'querystring'],
lookupCookie: 'user_lang',
cookieOptions: {
secure: true,
sameSite: 'Strict',
maxAge: 365 * 24 * 60 * 60, // 1 year
},
}))
.load(/* controllers */)
.listen(3000);
Inject the translation function directly:
@Injectable()
class UserService {
@I18n()
t!: TFn;
greet() {
return this.t('hello');
}
welcomeUser(name: string) {
return this.t('welcome', { name });
}
}
Automatically prefix all translation keys:
@Injectable()
class ErrorService {
@I18n('errors') // All keys prefixed with 'errors.'
t!: TFn;
notFound() {
return this.t('notFound'); // Translates 'errors.notFound'
}
forbidden() {
return this.t('forbidden'); // Translates 'errors.forbidden'
}
}
Get a specific translated value as a property:
@Injectable()
class HomeService {
@I18n('welcome.title', true) // Resolve 'welcome.title' key
title!: string; // Returns translated value
getTitle() {
return this.title; // No function call needed
}
}
Access deeply nested translations using dot notation:
const translations = {
en: {
user: {
profile: {
title: 'User Profile',
settings: {
privacy: 'Privacy Settings',
},
},
},
},
};
@Injectable()
class UserService {
@I18n() t!: TFn;
getProfileTitle() {
return this.t('user.profile.title');
}
getPrivacySettings() {
return this.t('user.profile.settings.privacy');
}
}
Use {{variable}} syntax for dynamic values:
const translations = {
en: {
greeting: 'Hello {{name}}, you have {{count}} messages',
itemCount: '{{count}} item(s) found',
},
};
@Injectable()
class MessageService {
@I18n()
t!: TFn;
greet(name: string, count: number) {
return this.t('greeting', { name, count });
}
showCount(count: number) {
return this.t('itemCount', { count });
}
}
The plugin detects language from multiple sources in configurable order:
# Cookie: language=fr
GET /api/greet
GET /api/greet?lang=fr
GET /api/greet
Accept-Language: fr-FR,fr;q=0.9,en;q=0.8
// With lookupFromPathIndex: 0
GET /fr/api/greet // Language: fr
GET /en/api/greet // Language: en
The I18nService provides methods to manage languages programmatically:
import { I18nService } from 'najm-i18n';
@Injectable()
class LanguageController {
constructor(private i18nService: I18nService) {}
// Get current language
getCurrentLang() {
return this.i18nService.getCurrentLanguage(); // e.g., 'en'
}
// Set language (updates cookie)
setLanguage(lang: string) {
const success = this.i18nService.setLanguage(lang);
return { success };
}
// Get available languages
getAvailableLanguages() {
return this.i18nService.getAvailableLanguages(); // e.g., ['en', 'fr', 'es']
}
// Check if language is supported
isSupported(lang: string) {
return this.i18nService.isLanguageSupported(lang);
}
// Get default language
getDefaultLang() {
return this.i18nService.getDefaultLanguage();
}
}
const translations = {
en: {
'user.name': 'Name',
'user.email': 'Email',
'button.submit': 'Submit',
},
};
const translations = {
en: {
user: {
name: 'Name',
email: 'Email',
profile: {
title: 'User Profile',
edit: 'Edit Profile',
},
},
button: {
submit: 'Submit',
cancel: 'Cancel',
},
},
};
// locales/en.json
{
"hello": "Hello",
"welcome": "Welcome {{name}}",
"errors": {
"notFound": "Not found",
"serverError": "Server error"
}
}
// Load in server
import en from './locales/en.json';
import fr from './locales/fr.json';
new Server()
.use(i18n({
translations: { en, fr },
defaultLanguage: 'en',
supportedLanguages: ['en', 'fr'],
}))
.load(/* controllers */)
.listen(3000);
import { Server, Controller, Get, Injectable } from 'najm-core';
import { i18n, I18n, type TFn } from 'najm-i18n';
const translations = {
en: {
welcome: 'Welcome to our API',
user: {
notFound: 'User not found',
created: 'User {{name}} created successfully',
},
},
fr: {
welcome: 'Bienvenue sur notre API',
user: {
notFound: 'Utilisateur non trouvé',
created: 'Utilisateur {{name}} créé avec succès',
},
},
};
@Injectable()
class UserService {
@I18n('user')
t!: TFn;
createUser(name: string) {
return this.t('created', { name });
}
notFound() {
return this.t('notFound');
}
}
@Controller('/api')
class ApiController {
@I18n()
t!: TFn;
constructor(private userService: UserService) {}
@Get('/welcome')
welcome() {
return { message: this.t('welcome') };
}
@Post('/users')
createUser(@Body() data: { name: string }) {
return { message: this.userService.createUser(data.name) };
}
@Get('/users/:id')
getUser(@Params('id') id: string) {
// Simulate user not found
throw new HttpError(404, this.userService.notFound());
}
}
new Server()
.use(i18n({
translations,
defaultLanguage: 'en',
supportedLanguages: ['en', 'fr'],
}))
.load(ApiController, UserService)
.listen(3000);
// Test:
// curl http://localhost:3000/api/welcome
// { "message": "Welcome to our API" }
// curl -H "Accept-Language: fr" http://localhost:3000/api/welcome
// { "message": "Bienvenue sur notre API" }
@I18n('domain') to reduce repetitionTFn type for translation functionsIf you're migrating from najm-core with built-in i18n:
// Before (core)
import { Server } from 'najm-core';
new Server({
i18n: {
translations: { /* ... */ },
defaultLanguage: 'en',
},
}).listen(3000);
// After (plugin)
import { Server } from 'najm-core';
import { i18n } from 'najm-i18n';
new Server()
.use(i18n({
translations: { /* ... */ },
defaultLanguage: 'en',
}))
.listen(3000);
MIT
FAQs
I18n .
We found that najm-i18n demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
Miasma Mini Shai-Hulud hits @immobiliarelabs Backstage plugins, targeting GitLab and LDAP auth packages on npm.

Security News
Rolldown paused Rust React Compiler integration after a 5MB binary size increase raised concerns about shipping React-specific code to all Vite users.

Security News
/Research
Mini Shai-Hulud expands into the Go ecosystem after hitting LeoPlatform npm packages and targeting GitHub Actions workflows.