Skip to content
better-i18n.com
En esta página

@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.

Setup #

Install the package #

bun 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({
  projectId: "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"; // [!code highlight]
import { i18n } from "./i18n";

export const auth = betterAuth({
  // ...your config
  plugins: [
    createBetterAuthProvider(i18n), // [!code highlight]
    // ...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

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.

Code
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" }

Locale Detection #

By default, the provider reads the Accept-Language header. For most apps, you'll want cookie-based detection instead — it uses the locale the user actually chose in your app, not their browser/OS language.

Pair with the provider's localeCookie prop — same cookie name on both sides:

src/auth.ts
createBetterAuthProvider(i18n, {
  localeCookie: "locale", // [!code highlight]
  // reads the "locale" cookie from the request
  // falls back to Accept-Language if cookie is missing
})
src/App.tsx
// Client writes the cookie automatically:
<BetterI18nProvider localeCookie> {/* default cookie name: "locale" */}
  <App />
</BetterI18nProvider>

Custom callback #

For advanced cases (database, user profile, custom header):

src/auth.ts
createBetterAuthProvider(i18n, {
  getLocale: async ({ headers }) => { // [!code highlight]
    const userId = getUserIdFromSession(headers);
    return await db.users.getLocale(userId) ?? "en";
  },
})

Options #

OptionTypeDefaultDescription
namespacestring"auth"CDN namespace for auth translations
localeCookiestringCookie name to read locale from (pairs with provider's localeCookie prop)
getLocale(ctx) => string | Promise<string>Accept-Language detectionCustom locale resolver (overrides all other detection)
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.

Full Key Reference (48 keys) #

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.
USER_ALREADY_EXISTS_USE_ANOTHER_EMAILUser already exists. Use another email.
INVALID_USERInvalid user
USER_EMAIL_NOT_FOUNDUser email not found
USER_ALREADY_HAS_PASSWORDUser already has a password. Provide that to delete the account.
FAILED_TO_CREATE_USERFailed to create user
FAILED_TO_UPDATE_USERFailed to update user
FAILED_TO_GET_USER_INFOFailed to get user info
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
FAILED_TO_UNLINK_LAST_ACCOUNTYou can't unlink your last account
PROVIDER_NOT_FOUNDProvider not found
ID_TOKEN_NOT_SUPPORTEDid_token not supported
Email Verification
VERIFICATION_EMAIL_NOT_ENABLEDVerification email isn't enabled
EMAIL_CAN_NOT_BE_UPDATEDEmail can not be updated
FAILED_TO_CREATE_VERIFICATIONUnable to create verification
URL Validation
INVALID_ORIGINInvalid origin
INVALID_CALLBACK_URLInvalid callbackURL
INVALID_REDIRECT_URLInvalid redirectURL
INVALID_ERROR_CALLBACK_URLInvalid errorCallbackURL
INVALID_NEW_USER_CALLBACK_URLInvalid newUserCallbackURL
MISSING_OR_NULL_ORIGINMissing or null Origin
CALLBACK_URL_REQUIREDcallbackURL is required
Security
CROSS_SITE_NAVIGATION_LOGIN_BLOCKEDCross-site navigation login blocked. This request appears to be a CSRF attack.
Validation
VALIDATION_ERRORValidation Error
MISSING_FIELDField is required
FIELD_NOT_ALLOWEDField not allowed to be set
ASYNC_VALIDATION_NOT_SUPPORTEDAsync validation is not supported
BODY_MUST_BE_AN_OBJECTBody must be an object
METHOD_NOT_ALLOWED_DEFER_SESSION_REQUIREDPOST method requires deferSessionRefresh to be enabled in session config

You can also access these programmatically:

TypeScript
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}`);
});

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 // [!code highlight]

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

app.use("*", betterI18n(i18n)); // Hono routes get c.get("t") // [!code highlight]

// Better Auth also uses the same i18n → shared CDN cache, zero extra fetches
  • Hono Middleware — Use the same i18n singleton with Hono for API response translations.
  • Express & Node.js — Node.js adapter for Express, Fastify, and Koa.
  • API Reference — Full reference for createServerI18n, Translator, and options.