A powerful and type-safe internationalization (i18n) middleware for Hono applications. Features automatic locale detection, namespace support, and seamless TypeScript integration.
- π Easy Integration - Simple setup with Hono applications
- π Automatic Locale Detection - Detects user locale from
Accept-Language
headers - π·οΈ Namespace Support - Organize translations by feature/module
- π Type Safety - Full TypeScript support with autocomplete
- π Parameter Interpolation - Dynamic message content with placeholders
- π― Quality Values Support - Respects
q
values in Accept-Language headers - π Fallback System - Graceful fallback to default locale
- β‘ Lightweight - Minimal dependencies and bundle size
npm install hono-intl
# or
pnpm add hono-intl
# or
yarn add hono-intl
// messages/en-US.ts
export const enUS = {
global: {
greeting: "Hello {name}!",
},
};
// messages/id-ID.ts
export const idID = {
global: {
greeting: "Halo {name}!",
},
};
import { Hono } from "hono";
import { createIntlMiddleware } from "hono-intl";
import { enUS } from "./messages/en-US";
import { idID } from "./messages/id-ID";
// Create middleware
export const intl = createIntlMiddleware({
locales: ["en-US", "id-ID"],
defaultLocale: "en-US",
messages: {
"en-US": enUS,
"id-ID": idID,
},
});
const app = new Hono();
// With namespace
app.get("/", intl("global"), async (c) => {
return c.json({
welcome: c.get("intl").get("welcome"),
greeting: c.get("intl").get("greeting", { name: "John" }),
});
});
// Without namespace (access full key path)
app.get("/status", intl(), async (c) => {
return c.json({
message: c.get("intl").get("global.welcome"),
});
});
Creates an internationalization middleware for Hono applications.
Parameter | Type | Required | Description |
---|---|---|---|
locales |
string[] |
β | Array of supported locale codes |
defaultLocale |
string |
β | Default locale to use as fallback |
messages |
Record<string, object> |
β | Translation messages for each locale |
selectLocale |
function |
β | Custom locale selection function |
A middleware function that can be used with or without namespaces.
// With namespace - access keys directly within the namespace
intlMiddleware("global");
// Without namespace - access full key paths
intlMiddleware();
Once the middleware is applied, you can access translations via c.get("intl")
:
const intl = c.get("intl");
// Simple message
const message = intl.get("welcome");
// Message with parameters
const greeting = intl.get("greeting", { name: "Alice" });
// Nested message
const error = intl.get("errors.notFound");
By default, the middleware automatically detects the user's preferred locale from the Accept-Language
header:
Accept-Language: id-ID,id;q=0.9,en;q=0.8
The middleware will:
- Parse quality values (
q
parameters) - Sort by preference (highest
q
value first) - Match against supported locales
- Support both exact matches (
id-ID
) and base language matches (id
βid-ID
) - Fallback to
defaultLocale
if no match found
You can provide a custom locale selection function:
const intlMiddleware = createIntlMiddleware({
locales: ["en-US", "id-ID", "fr-FR"],
defaultLocale: "en-US",
messages: {
/* ... */
},
selectLocale: ({ headers }) => {
// Custom logic based on headers, user preferences, etc.
const userLang = headers["x-user-language"];
if (userLang === "indonesian") return "id-ID";
if (userLang === "french") return "fr-FR";
return "en-US";
},
});
Organize your translations by features or modules using namespaces:
const messages = {
"en-US": {
auth: {
login: "Log In",
logout: "Log Out",
forgotPassword: "Forgot Password?",
},
dashboard: {
welcome: "Welcome back!",
stats: "Your Statistics",
},
},
};
// Use with specific namespace
app.post("/login", intlMiddleware("auth"), async (c) => {
return c.json({
button: c.get("intl").get("login"), // Gets "auth.login"
link: c.get("intl").get("forgotPassword"), // Gets "auth.forgotPassword"
});
});
Support dynamic content with parameter placeholders:
const messages = {
"en-US": {
user: {
welcome: "Welcome back, {name}!",
itemCount: "You have {count} {type} in your {location}",
notification: "Hello {user}, you have {count} new messages",
},
},
};
app.get("/profile", intlMiddleware("user"), async (c) => {
return c.json({
welcome: c.get("intl").get("welcome", { name: "Alice" }),
items: c.get("intl").get("itemCount", {
count: 5,
type: "items",
location: "cart",
}),
});
});
- Missing parameters: Placeholder remains unchanged (
{name}
) - Null/undefined values: Placeholder remains unchanged
- Other values: Converted to string using
String(value)
// Example with missing parameter
intl.get("Hello {name}!", { age: 25 }); // "Hello {name}!"
// Example with null value
intl.get("Hello {name}!", { name: null }); // "Hello {name}!"
// Example with number value
intl.get("Count: {count}", { count: 42 }); // "Count: 42"
The middleware provides multiple levels of fallback:
- Locale Fallback: If selected locale is unavailable, use
defaultLocale
- Message Fallback: If message key is not found, return the key itself
- Namespace Fallback: If using namespace and key not found, return key without namespace prefix
// If message doesn't exist
intl.get("nonexistent.key"); // Returns "nonexistent.key"
// With namespace - if "welcome" doesn't exist in "auth" namespace
intlMiddleware("auth");
intl.get("welcome"); // Returns "welcome" (not "auth.welcome")
app.post("/api/users", intlMiddleware("errors"), async (c) => {
try {
// ... user creation logic
} catch (error) {
return c.json(
{
error: c.get("intl").get("validation", { field: "email" }),
},
400
);
}
});
app.get("/api/products", intlMiddleware("products"), async (c) => {
const products = await getProducts();
return c.json({
title: c.get("intl").get("title"),
description: c.get("intl").get("description"),
products: products.map((p) => ({
...p,
statusText: c.get("intl").get(`status.${p.status}`),
})),
});
});
// Client sends: Accept-Language: id-ID,id;q=0.9,en;q=0.8
app.get("/welcome", intlMiddleware("global"), async (c) => {
return c.json({
message: c.get("intl").get("welcome"), // Will be in Indonesian
});
});
Full TypeScript support with autocomplete for message keys:
// TypeScript will provide autocomplete for available keys
const message = c.get("intl").get("global.welcome"); // β
Autocomplete
const error = c.get("intl").get("invalid.key"); // β TypeScript error
The library includes comprehensive tests. Run them with:
npm test
# or
pnpm test
Check out the /src/example
directory for a complete working example with:
- Message file organization
- Middleware setup
- Route implementation
- Namespace usage
Contributions are welcome! Please feel free to submit a Pull Request.
MIT License - see the LICENSE file for details.
Made with β€οΈ for the Hono community