Better Auth
इस पेज पर
@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/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({
projectId: "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"; // [!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 #
- 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
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" }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.
Cookie-based (recommended) #
Pair with the provider's localeCookie prop — same cookie name on both sides:
createBetterAuthProvider(i18n, {
localeCookie: "locale", // [!code highlight]
// reads the "locale" cookie from the request
// falls back to Accept-Language if cookie is missing
})// Client writes the cookie automatically:
<BetterI18nProvider localeCookie> {/* default cookie name: "locale" */}
<App />
</BetterI18nProvider>Custom callback #
For advanced cases (database, user profile, custom header):
createBetterAuthProvider(i18n, {
getLocale: async ({ headers }) => { // [!code highlight]
const userId = getUserIdFromSession(headers);
return await db.users.getLocale(userId) ?? "en";
},
})Options #
| Option | Type | Default | Description |
|---|---|---|---|
namespace | string | "auth" | CDN namespace for auth translations |
localeCookie | string | — | Cookie name to read locale from (pairs with provider's localeCookie prop) |
getLocale | (ctx) => string | Promise<string> | Accept-Language detection | Custom locale resolver (overrides all other detection) |
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.
Full Key Reference (48 keys) #
| 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. |
USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL | User already exists. Use another email. |
INVALID_USER | Invalid user |
USER_EMAIL_NOT_FOUND | User email not found |
USER_ALREADY_HAS_PASSWORD | User already has a password. Provide that to delete the account. |
FAILED_TO_CREATE_USER | Failed to create user |
FAILED_TO_UPDATE_USER | Failed to update user |
FAILED_TO_GET_USER_INFO | Failed to get user info |
| 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 |
FAILED_TO_UNLINK_LAST_ACCOUNT | You can't unlink your last account |
PROVIDER_NOT_FOUND | Provider not found |
ID_TOKEN_NOT_SUPPORTED | id_token not supported |
| Email Verification | |
VERIFICATION_EMAIL_NOT_ENABLED | Verification email isn't enabled |
EMAIL_CAN_NOT_BE_UPDATED | Email can not be updated |
FAILED_TO_CREATE_VERIFICATION | Unable to create verification |
| URL Validation | |
INVALID_ORIGIN | Invalid origin |
INVALID_CALLBACK_URL | Invalid callbackURL |
INVALID_REDIRECT_URL | Invalid redirectURL |
INVALID_ERROR_CALLBACK_URL | Invalid errorCallbackURL |
INVALID_NEW_USER_CALLBACK_URL | Invalid newUserCallbackURL |
MISSING_OR_NULL_ORIGIN | Missing or null Origin |
CALLBACK_URL_REQUIRED | callbackURL is required |
| Security | |
CROSS_SITE_NAVIGATION_LOGIN_BLOCKED | Cross-site navigation login blocked. This request appears to be a CSRF attack. |
| Validation | |
VALIDATION_ERROR | Validation Error |
MISSING_FIELD | Field is required |
FIELD_NOT_ALLOWED | Field not allowed to be set |
ASYNC_VALIDATION_NOT_SUPPORTED | Async validation is not supported |
BODY_MUST_BE_AN_OBJECT | Body must be an object |
METHOD_NOT_ALLOWED_DEFER_SESSION_REQUIRED | POST method requires deferSessionRefresh to be enabled in session config |
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}`);
});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 // [!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 fetchesRelated #
- 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.
Better I18N