Better I18NBetter I18N
Server SDK

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

src/i18n.ts
import { createServerI18n } from "@better-i18n/server";

export const i18n = createServerI18n({
  project: "my-org/api",
  defaultLocale: "en",
});

Register the middleware

src/app.ts
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

src/routes/users.ts
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:

src/types/hono.d.ts
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):

src/routes/translate.ts
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 });
});

On this page