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/servernpm install @better-i18n/serverpnpm add @better-i18n/serveryarn add @better-i18n/serverCreate the i18n singleton
This instance is shared between Better Auth and your server middleware (Hono, Express, etc.) — one CDN cache for everything.
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
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
- Go to your project in Better i18n
- Create a namespace called
auth - Add translation keys matching Better Auth error codes (e.g.,
INVALID_EMAIL_OR_PASSWORD) - Translate into your target languages
- 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:
- Detects if the response is an
APIErrorwith abody.code(e.g.,INVALID_EMAIL_OR_PASSWORD) - Resolves the user's locale from the
Accept-Languageheader - Fetches translations from the CDN — all namespaces are served in a single
translations.jsonper locale, and the provider scopes to theauthnamespace automatically - Replaces
body.messagewith 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:
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
| Option | Type | Default | Description |
|---|---|---|---|
namespace | string | "auth" | CDN namespace for auth translations |
getLocale | (ctx) => string | Promise<string> | Accept-Language detection | Custom locale resolver |
warnOnMissingKeys | boolean | true | Log 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
| Key | Default English Message |
|---|---|
| Authentication | |
INVALID_EMAIL_OR_PASSWORD | Invalid email or password |
INVALID_PASSWORD | Invalid password |
INVALID_EMAIL | Invalid email |
INVALID_TOKEN | Invalid token |
TOKEN_EXPIRED | Token expired |
EMAIL_NOT_VERIFIED | Email not verified |
EMAIL_ALREADY_VERIFIED | Email is already verified |
EMAIL_MISMATCH | Email mismatch |
| User Management | |
USER_NOT_FOUND | User not found |
USER_ALREADY_EXISTS | User already exists. |
INVALID_USER | Invalid user |
USER_EMAIL_NOT_FOUND | User email not found |
FAILED_TO_CREATE_USER | Failed to create user |
FAILED_TO_UPDATE_USER | Failed to update user |
| Session | |
SESSION_EXPIRED | Session expired. Re-authenticate to perform this action. |
SESSION_NOT_FRESH | Session is not fresh |
FAILED_TO_CREATE_SESSION | Failed to create session |
FAILED_TO_GET_SESSION | Failed to get session |
| Password | |
PASSWORD_TOO_SHORT | Password too short |
PASSWORD_TOO_LONG | Password too long |
PASSWORD_ALREADY_SET | User already has a password set |
CREDENTIAL_ACCOUNT_NOT_FOUND | Credential account not found |
| Account & Social | |
ACCOUNT_NOT_FOUND | Account not found |
SOCIAL_ACCOUNT_ALREADY_LINKED | Social account already linked |
LINKED_ACCOUNT_ALREADY_EXISTS | Linked account already exists |
PROVIDER_NOT_FOUND | Provider not found |
| Validation | |
VALIDATION_ERROR | Validation Error |
MISSING_FIELD | Field is required |
INVALID_CALLBACK_URL | Invalid 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:
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