Hono
Better i18n middleware for Hono
@better-i18n/server/hono provides a single betterI18n(i18n) middleware that detects the request locale from the Accept-Language header and injects locale and t into Hono's context variables.
Works on every Hono runtime. Hono uses Web Standards natively — c.req.raw.headers is already a Headers object, so no adapter layer is needed. The same middleware runs unchanged on:
- Cloudflare Workers (
hono/cloudflare-workers) - Deno Deploy (
hono/deno) - Bun (
hono/bun) - Node.js (
@hono/node-server)
Setup
Create the i18n singleton
import { createServerI18n } from "@better-i18n/server";
export const i18n = createServerI18n({
project: "my-org/api",
defaultLocale: "en",
});Register the middleware
import { Hono } from "hono";
import { betterI18n } from "@better-i18n/server/hono";
import { i18n } from "./i18n";
import type { Translator } from "@better-i18n/server";
const app = new Hono<{
Variables: {
locale: string;
t: Translator;
};
}>();
app.use("*", betterI18n(i18n));
export default app;The Hono<{ Variables: ... }> generic makes c.get("locale") and c.get("t") fully type-safe — no as casts needed.
Use in route handlers
import app from "../app";
app.get("/users/:id", async (c) => {
const t = c.get("t");
const locale = c.get("locale");
const user = await db.users.findById(c.req.param("id"));
if (!user) {
return c.json({ error: t("errors.notFound") }, 404);
}
return c.json({ user, locale });
});TypeScript Augmentation
If you use a shared app instance across multiple files, declare the variable types once in a .d.ts file to avoid repeating the generic:
import type { Translator } from "@better-i18n/server";
declare module "hono" {
interface ContextVariableMap {
locale: string;
t: Translator;
}
}With this declaration, c.get("locale") and c.get("t") are typed globally — no need to pass Hono<{ Variables: ... }> at each new Hono() call.
Locale Override Endpoint
For REST APIs where clients send a preferred locale in the request body or query params (bypassing Accept-Language):
import { i18n } from "../i18n";
// Override locale for a specific route
app.post("/send-email", async (c) => {
const { userId, locale: requestedLocale } = await c.req.json();
// Use explicitly requested locale — bypass Accept-Language detection
const t = await i18n.getTranslator(requestedLocale ?? c.get("locale"));
const subject = t("emails.welcome.subject");
const body = t("emails.welcome.body");
await sendEmail({ userId, subject, body });
return c.json({ sent: true });
});