Better I18NBetter I18N
Server SDK

Better Auth

Translate Better Auth error messages with CDN-based localization

@better-i18n/server/providers/better-auth creates a Better Auth plugin that automatically translates error messages (like "Invalid email or password") into your users' language — fetched from the CDN at runtime.

No redeployment needed. Unlike bundled localization packages, translations are served from the Better i18n CDN. Add a new language or fix a typo from the dashboard — it's live within 60 seconds.

Setup

Install the package

bun add @better-i18n/server
npm install @better-i18n/server
pnpm add @better-i18n/server
yarn add @better-i18n/server

Create the i18n singleton

This instance is shared between Better Auth and your server middleware (Hono, Express, etc.) — one CDN cache for everything.

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

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

Add the provider to Better Auth

src/auth.ts
import { betterAuth } from "better-auth";
import { createBetterAuthProvider } from "@better-i18n/server/providers/better-auth"; 
import { i18n } from "./i18n";

export const auth = betterAuth({
  // ...your config
  plugins: [
    createBetterAuthProvider(i18n), 
    // ...other plugins
  ],
});

That's it. The provider hooks into every Better Auth error response, detects the locale from the Accept-Language header, and translates the error message.

Add translations in the dashboard

  1. Go to your project in Better i18n
  2. Create a namespace called auth
  3. Add translation keys matching Better Auth error codes (e.g., INVALID_EMAIL_OR_PASSWORD)
  4. Translate into your target languages
  5. Publish — translations are live instantly

Seed keys with AI. Use the Better i18n MCP server to bulk-create all default keys in one prompt:

Create all keys from Better Auth's core error codes in the "auth" namespace. The keys are: INVALID_EMAIL_OR_PASSWORD, USER_NOT_FOUND, EMAIL_NOT_VERIFIED, SESSION_EXPIRED, PASSWORD_TOO_SHORT, PASSWORD_TOO_LONG, USER_ALREADY_EXISTS, INVALID_TOKEN, TOKEN_EXPIRED, ACCOUNT_NOT_FOUND. Set English values to their default messages.

How It Works

When Better Auth returns an error (e.g., invalid login), the provider:

  1. Detects if the response is an APIError with a body.code (e.g., INVALID_EMAIL_OR_PASSWORD)
  2. Resolves the user's locale from the Accept-Language header
  3. Fetches translations from the CDN — all namespaces are served in a single translations.json per locale, and the provider scopes to the auth namespace automatically
  4. Replaces body.message with the translated string

If a translation is missing, the original English message is preserved and a warning is logged — auth never breaks because of a missing translation.

Client (Accept-Language: tr) → Better Auth → APIError
  → Provider: detectLocale("tr")
  → CDN: /{org}/{project}/tr/translations.json → { "auth": { "INVALID_EMAIL_OR_PASSWORD": "..." } }
  → Response: { "code": "INVALID_EMAIL_OR_PASSWORD", "message": "Geçersiz e-posta veya şifre" }

Translations are cached in memory (60s TTL). After the first request, subsequent lookups are instant — no extra CDN calls per auth error.

Custom Locale Detection

By default, the provider reads the Accept-Language header. Override this when locale is stored elsewhere:

src/auth.ts
createBetterAuthProvider(i18n, {
  getLocale: async ({ headers }) => { 
    // Read from cookie, database, user profile, etc.
    const cookie = headers?.get("cookie") ?? "";
    const match = cookie.match(/locale=([a-z]{2})/);
    return match?.[1] ?? "en";
  },
})

Options

OptionTypeDefaultDescription
namespacestring"auth"CDN namespace for auth translations
getLocale(ctx) => string | Promise<string>Accept-Language detectionCustom locale resolver
warnOnMissingKeysbooleantrueLog warnings for untranslated error codes

Default Error Codes

The provider exports DEFAULT_AUTH_KEYS — all Better Auth core error codes with English defaults. Create these keys in your project's auth namespace, then translate them from the dashboard.

Seed with AI. Copy the prompt below into Claude, ChatGPT, or any AI agent with the Better i18n MCP server connected:

Create the following keys in the "auth" namespace of my project with their English source text, then translate them into Turkish, German, Spanish, and French:

INVALID_EMAIL_OR_PASSWORD = "Invalid email or password" USER_NOT_FOUND = "User not found" EMAIL_NOT_VERIFIED = "Email not verified" SESSION_EXPIRED = "Session expired. Re-authenticate to perform this action." PASSWORD_TOO_SHORT = "Password too short" PASSWORD_TOO_LONG = "Password too long" USER_ALREADY_EXISTS = "User already exists." INVALID_TOKEN = "Invalid token" TOKEN_EXPIRED = "Token expired" ACCOUNT_NOT_FOUND = "Account not found"

Full Key Reference

KeyDefault English Message
Authentication
INVALID_EMAIL_OR_PASSWORDInvalid email or password
INVALID_PASSWORDInvalid password
INVALID_EMAILInvalid email
INVALID_TOKENInvalid token
TOKEN_EXPIREDToken expired
EMAIL_NOT_VERIFIEDEmail not verified
EMAIL_ALREADY_VERIFIEDEmail is already verified
EMAIL_MISMATCHEmail mismatch
User Management
USER_NOT_FOUNDUser not found
USER_ALREADY_EXISTSUser already exists.
INVALID_USERInvalid user
USER_EMAIL_NOT_FOUNDUser email not found
FAILED_TO_CREATE_USERFailed to create user
FAILED_TO_UPDATE_USERFailed to update user
Session
SESSION_EXPIREDSession expired. Re-authenticate to perform this action.
SESSION_NOT_FRESHSession is not fresh
FAILED_TO_CREATE_SESSIONFailed to create session
FAILED_TO_GET_SESSIONFailed to get session
Password
PASSWORD_TOO_SHORTPassword too short
PASSWORD_TOO_LONGPassword too long
PASSWORD_ALREADY_SETUser already has a password set
CREDENTIAL_ACCOUNT_NOT_FOUNDCredential account not found
Account & Social
ACCOUNT_NOT_FOUNDAccount not found
SOCIAL_ACCOUNT_ALREADY_LINKEDSocial account already linked
LINKED_ACCOUNT_ALREADY_EXISTSLinked account already exists
PROVIDER_NOT_FOUNDProvider not found
Validation
VALIDATION_ERRORValidation Error
MISSING_FIELDField is required
INVALID_CALLBACK_URLInvalid callbackURL

You can also access these programmatically:

import { DEFAULT_AUTH_KEYS } from "@better-i18n/server/providers/better-auth";

// 40+ keys with English defaults
Object.entries(DEFAULT_AUTH_KEYS).forEach(([key, message]) => {
  console.log(`${key}: ${message}`);
});

This table covers Better Auth core error codes. Plugin-specific codes (admin, two-factor, email-otp, organization) can be added as additional keys in your namespace.

Sharing with Hono Middleware

The i18n singleton is designed to be shared. Use the same instance for both Better Auth and your Hono API:

src/app.ts
import { Hono } from "hono";
import { betterI18n } from "@better-i18n/server/hono";
import { i18n } from "./i18n"; // same singleton

const app = new Hono<{
  Variables: { locale: string; t: Translator };
}>();

app.use("*", betterI18n(i18n)); // Hono routes get c.get("t")

// Better Auth also uses the same i18n → shared CDN cache, zero extra fetches

On this page