π Node.js Runtime β
Use @i18n-micro/node to add i18n translations to any Node.js application, CLI tool, or backend service. This package provides the same translation logic as the Nuxt module, but for pure Node.js environments.
π¦ Installation β
pnpm add @i18n-micro/node
# or
npm install @i18n-micro/node
# or
yarn add @i18n-micro/nodeπ Quick Start β
import { createI18n } from '@i18n-micro/node'
// 1. Create I18n instance
const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
translationDir: './locales', // Path to your locales directory
})
// 2. Load translations from directory
await i18n.loadTranslations()
// 3. Use translations
console.log(i18n.t('greeting', { name: 'John' })) // "Hello, John!"
console.log(i18n.tc('apples', 5)) // "5 apples"π Translation File Structure β
The package supports the same file structure as the Nuxt module:
locales/
βββ en.json # Global English translations
βββ de.json # Global German translations
βββ pages/
βββ home/
β βββ en.json # Home page English translations
β βββ de.json # Home page German translations
βββ about/
βββ en.json # About page English translations
βββ de.json # About page German translationsπ§ API Reference β
createI18n(options: I18nOptions): I18n β
Creates a new I18n instance.
Options:
locale: string- Current localefallbackLocale?: string- Fallback locale (default: same as locale)translationDir?: string- Path to locales directoryplural?: PluralFunc- Custom pluralization functionmissingWarn?: boolean- Show warnings for missing translationsmissingHandler?: (locale: string, key: string, routeName: string) => void- Custom handler
i18n.loadTranslations(dir?: string): Promise<void> β
Load translations from directory (recursive, supports pages structure).
await i18n.loadTranslations() // Uses translationDir from constructor
await i18n.loadTranslations('./custom-locales') // Or specify custom pathi18n.t(key: string, params?: Params, defaultValue?: string | null, routeName?: string): string β
Get translation for a key. Uses currentRoute by default if routeName is not provided.
i18n.t('greeting', { name: 'John' }) // "Hello, John!"
i18n.t('welcome') // "Welcome"
i18n.t('nested.key') // Supports nested keys
i18n.t('title', undefined, undefined, 'home') // Route-specific translation
// With currentRoute
i18n.setRoute('home')
i18n.t('title') // Uses 'home' route automaticallyi18n.setRoute(routeName: string): void β
Set the current route name context. Useful when processing a specific page request.
i18n.setRoute('home') // Set current route to 'home'
i18n.t('title') // Will look for translation in 'home' routei18n.tc(key: string, count: number | Params, defaultValue?: string): string β
Plural translation.
// Translation: "no apples|one apple|{count} apples"
i18n.tc('apples', 0) // "no apples"
i18n.tc('apples', 1) // "one apple"
i18n.tc('apples', 5) // "5 apples"i18n.tn(value: number, options?: Intl.NumberFormatOptions): string β
Format number.
i18n.tn(1234.56) // "1,234.56"
i18n.tn(1234.56, { style: 'currency', currency: 'USD' }) // "$1,234.56"i18n.td(value: Date | number | string, options?: Intl.DateTimeFormatOptions): string β
Format date.
i18n.td(new Date()) // "1/15/2023"
i18n.td(new Date(), { year: 'numeric', month: 'long', day: 'numeric' }) // "January 15, 2023"i18n.tdr(value: Date | number | string, options?: Intl.RelativeTimeFormatOptions): string β
Format relative time.
i18n.tdr(new Date(Date.now() - 3600000)) // "1 hour ago"i18n.reload(): Promise<void> β
Clear cache and reload translations from disk. Use this when translation files change.
await i18n.reload() // Clears cache and reloads all filesπ‘ Usage Examples β
Express.js Middleware β
import express from 'express'
import { createI18n } from '@i18n-micro/node'
const app = express()
// Create I18n instance and load translations once at startup
const i18n = createI18n({
locale: 'en',
translationDir: './locales',
})
await i18n.loadTranslations()
// Middleware to set locale and route per request
app.use(async (req, res, next) => {
const locale = req.headers['accept-language']?.split(',')[0] || 'en'
const route = req.path.split('/').filter(Boolean)[0] || 'general'
i18n.locale = locale
i18n.setRoute(route)
req.i18n = i18n
await i18n.loadTranslations()
next()
})
app.get('/greet', (req, res) => {
res.json({ message: req.i18n.t('greeting', { name: 'World' }) })
})Fastify Plugin β
import Fastify from 'fastify'
import { createI18n } from '@i18n-micro/node'
const fastify = Fastify()
// Create I18n instance
const i18n = createI18n({
locale: 'en',
translationDir: './locales',
})
await i18n.loadTranslations()
// Plugin to add i18n to request
fastify.addHook('onRequest', async (request, reply) => {
const locale = request.headers['accept-language']?.split(',')[0] || 'en'
i18n.locale = locale
request.i18n = i18n
await i18n.loadTranslations()
})
fastify.get('/greet', async (request, reply) => {
return { message: request.i18n.t('greeting', { name: 'World' }) }
})Hot Module Replacement (HMR) with File Watcher β
import { createI18n } from '@i18n-micro/node'
import { watch } from 'node:fs'
// Create I18n instance
const i18n = createI18n({
locale: 'en',
translationDir: './locales',
})
// Initial load
await i18n.loadTranslations()
// Simple file watcher (Node.js built-in, no dependencies needed)
let reloadTimeout: NodeJS.Timeout
watch('./locales', { recursive: true }, (eventType, filename) => {
if (filename && filename.endsWith('.json')) {
// Debounce to avoid multiple reloads when saving one file
clearTimeout(reloadTimeout)
reloadTimeout = setTimeout(async () => {
console.log(`File changed: ${filename}, reloading...`)
await i18n.reload() // Clears cache and reloads all files
}, 100)
}
})
// Now i18n.t() will always return fresh data after file changesCLI Tool β
import { createI18n } from '@i18n-micro/node'
async function main() {
const i18n = createI18n({
locale: process.env.LOCALE || 'en',
translationDir: './locales',
})
await i18n.loadTranslations()
console.log(i18n.t('welcome'))
console.log(i18n.tc('items', 5))
}
main()π Using currentRoute β
The currentRoute feature allows you to set a route context that will be used by default in all t() calls:
import { createI18n } from '@i18n-micro/node'
const i18n = createI18n({
locale: 'en',
translationDir: './locales',
})
// Set route context
i18n.setRoute('home')
await i18n.loadTranslations()
// Now t() will automatically use 'home' route
console.log(i18n.t('title')) // Looks in 'home' route translations
console.log(i18n.t('welcome')) // Falls back to global translations
// Can still override with explicit routeName
console.log(i18n.t('title', undefined, undefined, 'about')) // Uses 'about' routeπ― Key Features β
- π Lightweight - No Vue dependencies, pure Node.js
- π¦ Same JSON structure - Use the same translation files as your Nuxt app
- π Route-specific translations - Support for page-level translations
- π Pluralization - Built-in plural form support
- π Formatting - Number, date, and relative time formatting
- β‘ Fast - Efficient in-memory caching
- π§ Type-safe - Full TypeScript support
- π Hot Reload - Built-in support for file watching and cache reloading
π Using in Nuxt Server Routes β
You can use @i18n-micro/node in Nuxt server API routes to provide server-side translations:
import { defineEventHandler, getQuery } from 'h3'
import { createI18n } from '@i18n-micro/node'
import { join } from 'node:path'
// Create a singleton I18n instance (in production, you'd want to cache this)
let i18nInstance = null
async function getI18n() {
if (!i18nInstance) {
// Get locales directory path (relative to playground root)
const localesPath = join(process.cwd(), 'playground', 'locales')
i18nInstance = createI18n({
locale: 'en',
fallbackLocale: 'en',
translationDir: localesPath,
})
}
return i18nInstance
}
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const locale = query.locale || 'en'
const route = query.route || 'general'
const i18n = await getI18n()
// Set locale and route for this request
i18n.locale = locale
i18n.setRoute(route)
// Load translations (required after setting route)
await i18n.loadTranslations()
// Demonstrate various translation methods
return {
locale: i18n.locale,
route: i18n.getRoute(),
translations: {
// Simple translation
welcome: i18n.t('welcome'),
// Translation with interpolation
greeting: i18n.t('greeting', { name: 'Node.js User' }),
// Nested key
nested: i18n.t('nested.message'),
// Pluralization
apples: {
zero: i18n.tc('apples', 0),
one: i18n.tc('apples', 1),
many: i18n.tc('apples', 5),
},
// Number formatting
number: i18n.tn(1234.56),
// Date formatting
date: i18n.td(new Date()),
// Relative time
relativeTime: i18n.tdr(new Date(Date.now() - 3600000)),
// Route-specific translation (if available)
routeSpecific: i18n.t('title', undefined, undefined, route),
},
// Show available methods
methods: {
hasTranslation: i18n.hasTranslation('welcome'),
currentRoute: i18n.getRoute(),
currentLocale: i18n.locale,
},
}
})π Locale Detection β
You can implement custom locale detection logic based on your needs:
import { createI18n } from '@i18n-micro/node'
import { IncomingMessage } from 'http'
function detectLocale(req: IncomingMessage): string {
// 1. Check URL parameters: ?locale=ru
const url = new URL(req.url || '', `http://${req.headers.host}`)
const localeFromQuery = url.searchParams.get('locale')
if (localeFromQuery) return localeFromQuery
// 2. Check cookies: user-locale cookie
const cookies = req.headers.cookie || ''
const localeFromCookie = cookies
.split(';')
.find(c => c.trim().startsWith('user-locale='))
?.split('=')[1]
if (localeFromCookie) return localeFromCookie
// 3. Check HTTP Headers: Accept-Language
const acceptLanguage = req.headers['accept-language']
if (acceptLanguage) {
const preferredLocale = acceptLanguage.split(',')[0].split('-')[0]
return preferredLocale
}
// 4. Fallback to default
return 'en'
}π Advanced Usage Examples β
Conditional Response Based on Locale β
import { createI18n } from '@i18n-micro/node'
const i18n = createI18n({
locale: 'en',
translationDir: './locales',
})
await i18n.loadTranslations()
function handleRequest(locale: string) {
i18n.locale = locale
// Return different content based on locale
if (locale === 'ru') {
return {
message: i18n.t('greeting', { name: 'ΠΠΈΡ' }),
locale: locale,
}
}
if (locale === 'de') {
return {
message: i18n.t('greeting', { name: 'Welt' }),
locale: locale,
}
}
// Default English response
return {
message: i18n.t('greeting', { name: 'World' }),
locale: locale,
}
}Locale-Aware API with Validation β
import { createI18n } from '@i18n-micro/node'
import { defineEventHandler, getQuery, createError } from 'h3'
const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
translationDir: './locales',
})
await i18n.loadTranslations()
const availableLocales = ['en', 'ru', 'de']
export default defineEventHandler((event) => {
const query = getQuery(event)
const locale = (query.locale as string) || 'en'
// Validate if the detected locale is supported
if (!availableLocales.includes(locale)) {
throw createError({
statusCode: 400,
statusMessage: `Unsupported locale: ${locale}. Available locales: ${availableLocales.join(', ')}`,
})
}
i18n.locale = locale
// Return locale-specific configuration
return {
locale: locale,
message: i18n.t('welcome'),
availableLocales: availableLocales,
}
})Integration with Route-Specific Translations β
import { createI18n } from '@i18n-micro/node'
import { defineEventHandler, getQuery } from 'h3'
const i18n = createI18n({
locale: 'en',
translationDir: './locales',
})
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const locale = (query.locale as string) || 'en'
const route = (query.route as string) || 'general'
// Set locale and route for this request
i18n.locale = locale
i18n.setRoute(route)
// Load translations (required after setting route)
await i18n.loadTranslations()
return {
locale: i18n.locale,
route: i18n.getRoute(),
// Route-specific translation
title: i18n.t('title'),
// Global translation (falls back if not in route)
welcome: i18n.t('welcome'),
}
})π Best Practices β
- Always validate locales: Check if the detected locale is in your available locales list
- Use fallback logic: Provide sensible defaults when locale detection fails
- Cache I18n instance: Create a singleton instance and reuse it across requests for better performance
- Handle edge cases: Account for unsupported locales and provide appropriate error responses
- Use route context: Set
currentRouteto automatically use route-specific translations - Reload on file changes: Use
reload()method with file watchers in development for hot-reloading
π Performance Considerations β
- Singleton pattern: Create one I18n instance and reuse it across requests
- Lazy loading: Load translations once at startup, not on every request
- Efficient caching: Translations are cached in memory for fast access
- Route context: Use
setRoute()to avoid passing route name to everyt()call - No external calls: All translations are loaded from local files, no network requests
π Related Documentation β
- Folder Structure - Learn about translation file organization
- Server Side Translations - Nuxt server-side usage
- API Reference - Complete method documentation