[GH-ISSUE #8492] i18n plugin doesn't translate errors thrown from plugin before hooks (e.g. username plugin's USERNAME_IS_ALREADY_TAKEN) #19735

Open
opened 2026-04-15 19:03:58 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @samerkh on GitHub (Mar 8, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8492

Originally assigned to: @himself65 on GitHub.

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Steps to reproduce

  1. Set up better-auth with the username and i18n plugins:
import { betterAuth } from "better-auth";
import { username } from "better-auth/plugins";
import { i18n } from "@better-auth/i18n";

export const auth = betterAuth({
  // ... database config
  emailAndPassword: { enabled: true },
  plugins: [
    username(),
    i18n({
      defaultLocale: "ar",
      detection: ["header"],
      translations: {
        ar: {
          USERNAME_IS_ALREADY_TAKEN: "اسم المستخدم مأخوذ بالفعل.",
          USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: "البريد الإلكتروني موجود مسبقًا.",
        },
      },
    }),
  ],
});
  1. Create a user:
 await authClient.signUp.email({
   email: "test@example.com",
   username: "testuser",
   password: "password123",
   name: "Test User",
 });
  1. Sign up with the same username, different email — error is NOT translated:
 await authClient.signUp.email({
   email: "other@example.com",
   username: "testuser", // duplicate username
   password: "password123",
   name: "Test User 2",
 });
 // Response:
 // { code: "USERNAME_IS_ALREADY_TAKEN", message: "Username is already taken. Please try another." }
  1. Sign up with a different username, same email — error IS translated:
 await authClient.signUp.email({
   email: "test@example.com", // duplicate email
   username: "otheruser",
   password: "password123",
   name: "Test User 3",
 });
 // Response:
 // { code: "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL", message: "البريد الإلكتروني موجود مسبقًا.", originalMessage: "User already exists. Use another email."
  }

Step 3 is not translated because the username duplicate check runs in a before hook (bypasses i18n's after hook). Step 4 is translated because the email
duplicate check runs inside the endpoint handler (i18n's after hook processes it).

Current vs. Expected behavior

Current behavior

Both duplicate username and duplicate email return an error with a code field, but only the email error gets translated by the i18n plugin:

Duplicate username — returns the original English message, no originalMessage field:

{
  "code": "USERNAME_IS_ALREADY_TAKEN",
  "message": "Username is already taken. Please try another."
}

Duplicate email — correctly translated, includes originalMessage:

  {
    "code": "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL",
    "message": "البريد الإلكتروني موجود مسبقًا.",
    "originalMessage": "User already exists. Use another email."
  }

Expected behavior

Both errors should be translated consistently:

Duplicate username:

  {
    "code": "USERNAME_IS_ALREADY_TAKEN",
    "message": "اسم المستخدم مأخوذ بالفعل.",
    "originalMessage": "Username is already taken. Please try another."
  }

Duplicate email:

  {
    "code": "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL",
    "message": "البريد الإلكتروني موجود مسبقًا.",
    "originalMessage": "User already exists. Use another email."
  }

What version of Better Auth are you using?

1.5.4

System info

{
  "system": {
    "platform": "win32",
    "arch": "x64",
    "version": "Windows 11 Home",
    "release": "10.0.26200",
    "cpuCount": 12,
    "cpuModel": "Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz",
    "totalMemory": "15.85 GB",
    "freeMemory": "2.80 GB"
  },
  "node": {
    "version": "v24.11.1",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.6.2"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "16.1.5"
    },
    {
      "name": "react",
      "version": "19.2.1"
    }
  ],
  "databases": [
    {
      "name": "pg",
      "version": "^8.16.3"
    },
    {
      "name": "@prisma/client",
      "version": "^7.4.2"
    }
  ],
  "betterAuth": {
    "version": "^1.5.4",
    "config": {
      "emailAndPassword": {
        "enabled": true
      },
      "plugins": [
        {
          "name": "username",
          "config": {
            "id": "username",
            "endpoints": {},
            "schema": {
              "user": {
                "fields": {
                  "username": {
                    "type": "string",
                    "required": false,
                    "sortable": true,
                    "unique": true,
                    "returned": true,
                    "transform": {}
                  },
                  "displayUsername": {
                    "type": "string",
                    "required": false,
                    "transform": {}
                  }
                }
              }
            },
            "hooks": {
              "before": [
                {},
                {}
              ]
            },
            "$ERROR_CODES": {
              "INVALID_USERNAME_OR_PASSWORD": {
                "code": "INVALID_USERNAME_OR_PASSWORD",
                "message": "اسم المستخدم أو كلمة المرور غير صالحة"
              },
              "EMAIL_NOT_VERIFIED": {
                "code": "EMAIL_NOT_VERIFIED",
                "message": "البريد الإلكتروني لم يتم التحقق منه"
              },
              "UNEXPECTED_ERROR": {
                "code": "UNEXPECTED_ERROR",
                "message": "Unexpected error"
              },
              "USERNAME_IS_ALREADY_TAKEN": {
                "code": "USERNAME_IS_ALREADY_TAKEN",
                "message": "اسم المستخدم مأخوذ بالفعل. الرجاء اختيار اسم آخر."
              },
              "USERNAME_TOO_SHORT": {
                "code": "USERNAME_TOO_SHORT",
                "message": "اسم المستخدم قصير جدًا"
              },
              "USERNAME_TOO_LONG": {
                "code": "USERNAME_TOO_LONG",
                "message": "اسم المستخدم طويل جدًا"
              },
              "INVALID_USERNAME": {
                "code": "INVALID_USERNAME",
                "message": "اسم المستخدم غير صالح"
              },
              "INVALID_DISPLAY_USERNAME": {
                "code": "INVALID_DISPLAY_USERNAME",
                "message": "اسم العرض غير صالح"
              }
            }
          }
        },
        {
          "name": "admin",
          "config": {
            "id": "admin",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "$ERROR_CODES": {
              "FAILED_TO_CREATE_USER": {
                "code": "FAILED_TO_CREATE_USER",
                "message": "فشل في إنشاء حساب المستخدم"
              },
              "USER_ALREADY_EXISTS": {
                "code": "USER_ALREADY_EXISTS",
                "message": "المستخدم موجود مسبقًا"
              },
              "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": {
                "code": "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL",
                "message": "البريد الإلكتروني موجود مسبقًا. استخدم بريدًا إلكترونيًا آخر."
              },
              "YOU_CANNOT_BAN_YOURSELF": {
                "code": "YOU_CANNOT_BAN_YOURSELF",
                "message": "لا يمكنك حظر نفسك"
              },
              "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE",
                "message": "غير مصرّح لك بتغيير أدوار المستخدمين"
              },
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS",
                "message": "غير مصرّح لك بإنشاء مستخدمين"
              },
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS",
                "message": "غير مصرّح لك بعرض قائمة المستخدمين"
              },
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS",
                "message": "غير مصرّح لك بعرض جلسات المستخدمين"
              },
              "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS",
                "message": "غير مصرّح لك بحظر المستخدمين"
              },
              "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS",
                "message": "غير مصرّح لك بانتحال هوية المستخدمين"
              },
              "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS",
                "message": "غير مصرّح لك بإلغاء جلسات المستخدمين"
              },
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS",
                "message": "غير مصرّح لك بحذف المستخدمين"
              },
              "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD",
                "message": "غير مصرّح لك بتعيين كلمة مرور المستخدمين"
              },
              "BANNED_USER": {
                "code": "BANNED_USER",
                "message": "تم حظرك من هذا التطبيق"
              },
              "YOU_ARE_NOT_ALLOWED_TO_GET_USER": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_GET_USER",
                "message": "غير مصرّح لك بعرض المستخدم"
              },
              "NO_DATA_TO_UPDATE": {
                "code": "NO_DATA_TO_UPDATE",
                "message": "لا توجد بيانات للتحديث"
              },
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS",
                "message": "غير مصرّح لك بتحديث المستخدمين"
              },
              "YOU_CANNOT_REMOVE_YOURSELF": {
                "code": "YOU_CANNOT_REMOVE_YOURSELF",
                "message": "لا يمكنك إزالة نفسك"
              },
              "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE": {
                "code": "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE",
                "message": "غير مصرّح لك بتعيين قيمة دور غير موجودة"
              },
              "YOU_CANNOT_IMPERSONATE_ADMINS": {
                "code": "YOU_CANNOT_IMPERSONATE_ADMINS",
                "message": "لا يمكنك انتحال هوية المشرفين"
              },
              "INVALID_ROLE_TYPE": {
                "code": "INVALID_ROLE_TYPE",
                "message": "نوع الدور غير صالح"
              }
            },
            "schema": {
              "user": {
                "fields": {
                  "role": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banned": {
                    "type": "boolean",
                    "defaultValue": false,
                    "required": false,
                    "input": false
                  },
                  "banReason": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banExpires": {
                    "type": "date",
                    "required": false,
                    "input": false
                  }
                }
              },
              "session": {
                "fields": {
                  "impersonatedBy": {
                    "type": "string",
                    "required": false
                  }
                }
              }
            },
            "options": {
              "ac": {
                "statements": {
                  "user": [
                    "create",
                    "list",
                    "set-role",
                    "ban",
                    "impersonate",
                    "impersonate-admins",
                    "delete",
                    "set-password",
                    "get",
                    "update"
                  ],
                  "session": [
                    "list",
                    "revoke",
                    "delete"
                  ]
                }
              },
              "roles": {
                "ADMIN": {
                  "statements": {
                    "user": [
                      "create",
                      "list",
                      "set-role",
                      "ban",
                      "impersonate",
                      "delete",
                      "set-password",
                      "get",
                      "update"
                    ],
                    "session": [
                      "list",
                      "revoke",
                      "delete"
                    ]
                  }
                },
                "USER": {
                  "statements": {
                    "user": [],
                    "session": []
                  }
                }
              },
              "defaultRole": "USER"
            }
          }
        },
        {
          "name": "i18n",
          "config": {
            "id": "i18n",
            "hooks": {
              "after": [
                {}
              ]
            },
            "options": {
              "defaultLocale": "ar",
              "detection": [
                "header"
              ],
              "localeCookie": "locale",
              "userLocaleField": "locale",
              "translations": {
                "ar": {
                  "USER_NOT_FOUND": "المستخدم غير موجود",
                  "FAILED_TO_CREATE_USER": "فشل في إنشاء حساب المستخدم",
                  "FAILED_TO_CREATE_SESSION": "فشل في إنشاء الجلسة",
                  "FAILED_TO_UPDATE_USER": "فشل في تحديث بيانات المستخدم",
                  "FAILED_TO_GET_SESSION": "فشل في استرجاع الجلسة",
                  "INVALID_PASSWORD": "[REDACTED]",
                  "INVALID_EMAIL": "البريد الإلكتروني غير صالح",
                  "INVALID_EMAIL_OR_PASSWORD": "[REDACTED]",
                  "INVALID_USER": "مستخدم غير صالح",
                  "SOCIAL_ACCOUNT_ALREADY_LINKED": "الحساب الاجتماعي مرتبط بالفعل",
                  "PROVIDER_NOT_FOUND": "مزود الخدمة غير موجود",
                  "INVALID_TOKEN": "[REDACTED]",
                  "TOKEN_EXPIRED": "انتهت صلاحية الرمز",
                  "ID_TOKEN_NOT_SUPPORTED": "الرمز المميز غير مدعوم",
                  "FAILED_TO_GET_USER_INFO": "فشل في استرجاع معلومات المستخدم",
                  "USER_EMAIL_NOT_FOUND": "البريد الإلكتروني للمستخدم غير موجود",
                  "EMAIL_NOT_VERIFIED": "البريد الإلكتروني لم يتم التحقق منه",
                  "PASSWORD_TOO_SHORT": "كلمة المرور قصيرة جدًا",
                  "PASSWORD_TOO_LONG": "كلمة المرور طويلة جدًا",
                  "USER_ALREADY_EXISTS": "المستخدم موجود مسبقًا",
                  "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "البريد الإلكتروني موجود مسبقًا. استخدم بريدًا إلكترونيًا آخر.",  
                  "EMAIL_CAN_NOT_BE_UPDATED": "لا يمكن تحديث البريد الإلكتروني",
                  "CREDENTIAL_ACCOUNT_NOT_FOUND": "حساب بيانات الاعتماد غير موجود",
                  "SESSION_EXPIRED": "انتهت صلاحية الجلسة. يرجى تسجيل الدخول مجددًا لمتابعة هذا الإجراء.",
                  "FAILED_TO_UNLINK_LAST_ACCOUNT": "لا يمكن إلغاء ربط الحساب الأخير",
                  "ACCOUNT_NOT_FOUND": "الحساب غير موجود",
                  "USER_ALREADY_HAS_PASSWORD": "[REDACTED]",
                  "CROSS_SITE_NAVIGATION_LOGIN_BLOCKED": "تم حظر تسجيل الدخول عبر المواقع. يبدو أن هذا الطلب هجوم CSRF.",    
                  "VERIFICATION_EMAIL_NOT_ENABLED": "بريد التحقق غير مفعّل",
                  "EMAIL_ALREADY_VERIFIED": "البريد الإلكتروني تم التحقق منه بالفعل",
                  "EMAIL_MISMATCH": "البريد الإلكتروني غير متطابق",
                  "SESSION_NOT_FRESH": "الجلسة ليست حديثة",
                  "LINKED_ACCOUNT_ALREADY_EXISTS": "الحساب المرتبط موجود بالفعل",
                  "INVALID_ORIGIN": "مصدر غير صالح",
                  "INVALID_CALLBACK_URL": "رابط إعادة التوجيه غير صالح",
                  "INVALID_REDIRECT_URL": "رابط إعادة التوجيه غير صالح",
                  "INVALID_ERROR_CALLBACK_URL": "رابط خطأ إعادة التوجيه غير صالح",
                  "INVALID_NEW_USER_CALLBACK_URL": "رابط المستخدم الجديد غير صالح",
                  "MISSING_OR_NULL_ORIGIN": "المصدر مفقود أو فارغ",
                  "CALLBACK_URL_REQUIRED": "رابط إعادة التوجيه مطلوب",
                  "FAILED_TO_CREATE_VERIFICATION": "تعذّر إنشاء عملية التحقق",
                  "FIELD_NOT_ALLOWED": "هذا الحقل غير مسموح بتعيينه",
                  "VALIDATION_ERROR": "خطأ في التحقق",
                  "MISSING_FIELD": "هذا الحقل مطلوب",
                  "PASSWORD_ALREADY_SET": "المستخدم لديه كلمة مرور مسبقًا",
                  "INVALID_USERNAME_OR_PASSWORD": "[REDACTED]",
                  "USERNAME_IS_ALREADY_TAKEN": "اسم المستخدم مأخوذ بالفعل. الرجاء اختيار اسم آخر.",
                  "USERNAME_TOO_SHORT": "اسم المستخدم قصير جدًا",
                  "USERNAME_TOO_LONG": "اسم المستخدم طويل جدًا",
                  "INVALID_USERNAME": "اسم المستخدم غير صالح",
                  "INVALID_DISPLAY_USERNAME": "اسم العرض غير صالح",
                  "YOU_CANNOT_BAN_YOURSELF": "لا يمكنك حظر نفسك",
                  "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "غير مصرّح لك بتغيير أدوار المستخدمين",
                  "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "غير مصرّح لك بإنشاء مستخدمين",
                  "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "غير مصرّح لك بعرض قائمة المستخدمين",
                  "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "غير مصرّح لك بعرض جلسات المستخدمين",
                  "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "غير مصرّح لك بحظر المستخدمين",
                  "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "غير مصرّح لك بانتحال هوية المستخدمين",
                  "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "غير مصرّح لك بإلغاء جلسات المستخدمين",
                  "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "غير مصرّح لك بحذف المستخدمين",
                  "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]",
                  "BANNED_USER": "تم حظرك من هذا التطبيق",
                  "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "غير مصرّح لك بعرض المستخدم",
                  "NO_DATA_TO_UPDATE": "لا توجد بيانات للتحديث",
                  "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "غير مصرّح لك بتحديث المستخدمين",
                  "YOU_CANNOT_REMOVE_YOURSELF": "لا يمكنك إزالة نفسك",
                  "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE": "غير مصرّح لك بتعيين قيمة دور غير موجودة",
                  "YOU_CANNOT_IMPERSONATE_ADMINS": "لا يمكنك انتحال هوية المشرفين",
                  "INVALID_ROLE_TYPE": "نوع الدور غير صالح"
                }
              }
            }
          }
        },
        {
          "name": "next-cookies",
          "config": {
            "id": "next-cookies",
            "hooks": {
              "before": [
                {}
              ],
              "after": [
                {}
              ]
            }
          }
        }
      ],
      "advanced": {
        "database": {
          "generateId": false
        }
      }
    }
  }
}

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)

import { PrismaClient } from "@/app/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { nextCookies } from "better-auth/next-js";
import { admin, username } from "better-auth/plugins";
import "dotenv/config";
import { authCustomConfig } from "./auth-config";

const prismaPostgresAdapter = new PrismaPg({
  connectionString: process.env.DATABASE_URL,
});

const prisma = new PrismaClient({
  adapter: prismaPostgresAdapter,
});

export const auth = betterAuth({
  database: prismaAdapter(prisma, {
    provider: "postgresql",
  }),
  emailAndPassword: {
    enabled: true,
  },
  plugins: [
    username(),
    admin({
      ...authCustomConfig,
      defaultRole: "USER",
    }),
    nextCookies(), // keep this plugin last
  ],
  advanced: {
    database: {
      generateId: false,
    },
  },
});

Additional context

Environment

  • better-auth: 1.5.4
  • @better-auth/i18n: 1.5.4
  • Plugins: username, admin, i18n

Describe the bug

The @better-auth/i18n plugin does not translate error messages thrown from plugin before hooks. Only errors thrown from endpoint handlers are
translated.

For example, during /sign-up/email:

  • USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL — thrown from the endpoint handler → translated
  • USERNAME_IS_ALREADY_TAKEN — thrown from the username plugin's before hook → not translated

// Translated (endpoint error):
[APIError] {
code: 'USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL',
message: 'البريد الإلكتروني موجود مسبقًا. استخدم بريدًا إلكترونيًا آخر.',
originalMessage: 'User already exists. Use another email.'
}

// NOT translated (before hook error):
[APIError] {
code: 'USERNAME_IS_ALREADY_TAKEN',
message: 'Username is already taken. Please try another.'
// no originalMessage, no translation applied
}

Root cause

The i18n plugin registers only an after hook (hooks: { after: [...] }). In to-auth-endpoints.ts, the execution flow is:

  1. runBeforeHooks() — username plugin's before hook throws APIError
  2. The error propagates immediately — runAfterHooks() never executes
  3. The i18n after-hook never gets a chance to translate

Compare with endpoint errors: they are caught (lines 58-69 in to-auth-endpoints.mjs), stored in context.returned, and then runAfterHooks() runs — so
i18n can translate them.

Affected error codes

All errors thrown from the username plugin's before hook (lines 188-217 in dist/plugins/username/index.mjs) on /sign-up/email and /update-user:

  • USERNAME_IS_ALREADY_TAKEN
  • USERNAME_TOO_SHORT
  • USERNAME_TOO_LONG
  • INVALID_USERNAME
  • INVALID_DISPLAY_USERNAME

This likely affects any plugin that throws APIError from a before hook, not just the username plugin.

Expected behavior

All error codes should be translated by the i18n plugin regardless of whether they originate from a before hook, endpoint handler, or after hook.

Possible fix

In runBeforeHooks() (in to-auth-endpoints.ts), before-hook APIErrors should be caught and routed through runAfterHooks() the same way endpoint
errors are, so the i18n plugin (and any other after-hook-based plugin) can process them.

Additional context

  • This is the same class of bug as #7969 (captcha errors not translated), which was closed with a captcha-specific fix rather than addressing the
    underlying before-hook bypass.
  • The third-party package better-auth-localization also uses after hooks and has the same
    limitation — the username plugin's USERNAME_IS_ALREADY_TAKEN translation exists in their language files but can never actually be applied at runtime.
Originally created by @samerkh on GitHub (Mar 8, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8492 Originally assigned to: @himself65 on GitHub. ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce ### Steps to reproduce 1. Set up better-auth with the username and i18n plugins: ```ts import { betterAuth } from "better-auth"; import { username } from "better-auth/plugins"; import { i18n } from "@better-auth/i18n"; export const auth = betterAuth({ // ... database config emailAndPassword: { enabled: true }, plugins: [ username(), i18n({ defaultLocale: "ar", detection: ["header"], translations: { ar: { USERNAME_IS_ALREADY_TAKEN: "اسم المستخدم مأخوذ بالفعل.", USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: "البريد الإلكتروني موجود مسبقًا.", }, }, }), ], }); ``` 2. Create a user: ```ts await authClient.signUp.email({ email: "test@example.com", username: "testuser", password: "password123", name: "Test User", }); ``` 3. Sign up with the same username, different email — error is NOT translated: ```ts await authClient.signUp.email({ email: "other@example.com", username: "testuser", // duplicate username password: "password123", name: "Test User 2", }); // Response: // { code: "USERNAME_IS_ALREADY_TAKEN", message: "Username is already taken. Please try another." } ``` 4. Sign up with a different username, same email — error IS translated: ```ts await authClient.signUp.email({ email: "test@example.com", // duplicate email username: "otheruser", password: "password123", name: "Test User 3", }); // Response: // { code: "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL", message: "البريد الإلكتروني موجود مسبقًا.", originalMessage: "User already exists. Use another email." } ``` Step 3 is not translated because the username duplicate check runs in a before hook (bypasses i18n's after hook). Step 4 is translated because the email duplicate check runs inside the endpoint handler (i18n's after hook processes it). ### Current vs. Expected behavior ### Current behavior Both duplicate username and duplicate email return an error with a `code` field, but only the email error gets translated by the i18n plugin: **Duplicate username** — returns the original English message, no `originalMessage` field: ```json { "code": "USERNAME_IS_ALREADY_TAKEN", "message": "Username is already taken. Please try another." } ``` Duplicate email — correctly translated, includes originalMessage: ```json { "code": "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL", "message": "البريد الإلكتروني موجود مسبقًا.", "originalMessage": "User already exists. Use another email." } ``` ### Expected behavior Both errors should be translated consistently: Duplicate username: ```json { "code": "USERNAME_IS_ALREADY_TAKEN", "message": "اسم المستخدم مأخوذ بالفعل.", "originalMessage": "Username is already taken. Please try another." } ``` Duplicate email: ```json { "code": "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL", "message": "البريد الإلكتروني موجود مسبقًا.", "originalMessage": "User already exists. Use another email." } ``` ### What version of Better Auth are you using? 1.5.4 ### System info ```bash { "system": { "platform": "win32", "arch": "x64", "version": "Windows 11 Home", "release": "10.0.26200", "cpuCount": 12, "cpuModel": "Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz", "totalMemory": "15.85 GB", "freeMemory": "2.80 GB" }, "node": { "version": "v24.11.1", "env": "development" }, "packageManager": { "name": "npm", "version": "11.6.2" }, "frameworks": [ { "name": "next", "version": "16.1.5" }, { "name": "react", "version": "19.2.1" } ], "databases": [ { "name": "pg", "version": "^8.16.3" }, { "name": "@prisma/client", "version": "^7.4.2" } ], "betterAuth": { "version": "^1.5.4", "config": { "emailAndPassword": { "enabled": true }, "plugins": [ { "name": "username", "config": { "id": "username", "endpoints": {}, "schema": { "user": { "fields": { "username": { "type": "string", "required": false, "sortable": true, "unique": true, "returned": true, "transform": {} }, "displayUsername": { "type": "string", "required": false, "transform": {} } } } }, "hooks": { "before": [ {}, {} ] }, "$ERROR_CODES": { "INVALID_USERNAME_OR_PASSWORD": { "code": "INVALID_USERNAME_OR_PASSWORD", "message": "اسم المستخدم أو كلمة المرور غير صالحة" }, "EMAIL_NOT_VERIFIED": { "code": "EMAIL_NOT_VERIFIED", "message": "البريد الإلكتروني لم يتم التحقق منه" }, "UNEXPECTED_ERROR": { "code": "UNEXPECTED_ERROR", "message": "Unexpected error" }, "USERNAME_IS_ALREADY_TAKEN": { "code": "USERNAME_IS_ALREADY_TAKEN", "message": "اسم المستخدم مأخوذ بالفعل. الرجاء اختيار اسم آخر." }, "USERNAME_TOO_SHORT": { "code": "USERNAME_TOO_SHORT", "message": "اسم المستخدم قصير جدًا" }, "USERNAME_TOO_LONG": { "code": "USERNAME_TOO_LONG", "message": "اسم المستخدم طويل جدًا" }, "INVALID_USERNAME": { "code": "INVALID_USERNAME", "message": "اسم المستخدم غير صالح" }, "INVALID_DISPLAY_USERNAME": { "code": "INVALID_DISPLAY_USERNAME", "message": "اسم العرض غير صالح" } } } }, { "name": "admin", "config": { "id": "admin", "hooks": { "after": [ {} ] }, "endpoints": {}, "$ERROR_CODES": { "FAILED_TO_CREATE_USER": { "code": "FAILED_TO_CREATE_USER", "message": "فشل في إنشاء حساب المستخدم" }, "USER_ALREADY_EXISTS": { "code": "USER_ALREADY_EXISTS", "message": "المستخدم موجود مسبقًا" }, "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": { "code": "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL", "message": "البريد الإلكتروني موجود مسبقًا. استخدم بريدًا إلكترونيًا آخر." }, "YOU_CANNOT_BAN_YOURSELF": { "code": "YOU_CANNOT_BAN_YOURSELF", "message": "لا يمكنك حظر نفسك" }, "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": { "code": "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE", "message": "غير مصرّح لك بتغيير أدوار المستخدمين" }, "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": { "code": "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS", "message": "غير مصرّح لك بإنشاء مستخدمين" }, "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": { "code": "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS", "message": "غير مصرّح لك بعرض قائمة المستخدمين" }, "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": { "code": "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS", "message": "غير مصرّح لك بعرض جلسات المستخدمين" }, "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": { "code": "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS", "message": "غير مصرّح لك بحظر المستخدمين" }, "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": { "code": "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS", "message": "غير مصرّح لك بانتحال هوية المستخدمين" }, "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": { "code": "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS", "message": "غير مصرّح لك بإلغاء جلسات المستخدمين" }, "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": { "code": "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS", "message": "غير مصرّح لك بحذف المستخدمين" }, "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": { "code": "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD", "message": "غير مصرّح لك بتعيين كلمة مرور المستخدمين" }, "BANNED_USER": { "code": "BANNED_USER", "message": "تم حظرك من هذا التطبيق" }, "YOU_ARE_NOT_ALLOWED_TO_GET_USER": { "code": "YOU_ARE_NOT_ALLOWED_TO_GET_USER", "message": "غير مصرّح لك بعرض المستخدم" }, "NO_DATA_TO_UPDATE": { "code": "NO_DATA_TO_UPDATE", "message": "لا توجد بيانات للتحديث" }, "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": { "code": "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS", "message": "غير مصرّح لك بتحديث المستخدمين" }, "YOU_CANNOT_REMOVE_YOURSELF": { "code": "YOU_CANNOT_REMOVE_YOURSELF", "message": "لا يمكنك إزالة نفسك" }, "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE": { "code": "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE", "message": "غير مصرّح لك بتعيين قيمة دور غير موجودة" }, "YOU_CANNOT_IMPERSONATE_ADMINS": { "code": "YOU_CANNOT_IMPERSONATE_ADMINS", "message": "لا يمكنك انتحال هوية المشرفين" }, "INVALID_ROLE_TYPE": { "code": "INVALID_ROLE_TYPE", "message": "نوع الدور غير صالح" } }, "schema": { "user": { "fields": { "role": { "type": "string", "required": false, "input": false }, "banned": { "type": "boolean", "defaultValue": false, "required": false, "input": false }, "banReason": { "type": "string", "required": false, "input": false }, "banExpires": { "type": "date", "required": false, "input": false } } }, "session": { "fields": { "impersonatedBy": { "type": "string", "required": false } } } }, "options": { "ac": { "statements": { "user": [ "create", "list", "set-role", "ban", "impersonate", "impersonate-admins", "delete", "set-password", "get", "update" ], "session": [ "list", "revoke", "delete" ] } }, "roles": { "ADMIN": { "statements": { "user": [ "create", "list", "set-role", "ban", "impersonate", "delete", "set-password", "get", "update" ], "session": [ "list", "revoke", "delete" ] } }, "USER": { "statements": { "user": [], "session": [] } } }, "defaultRole": "USER" } } }, { "name": "i18n", "config": { "id": "i18n", "hooks": { "after": [ {} ] }, "options": { "defaultLocale": "ar", "detection": [ "header" ], "localeCookie": "locale", "userLocaleField": "locale", "translations": { "ar": { "USER_NOT_FOUND": "المستخدم غير موجود", "FAILED_TO_CREATE_USER": "فشل في إنشاء حساب المستخدم", "FAILED_TO_CREATE_SESSION": "فشل في إنشاء الجلسة", "FAILED_TO_UPDATE_USER": "فشل في تحديث بيانات المستخدم", "FAILED_TO_GET_SESSION": "فشل في استرجاع الجلسة", "INVALID_PASSWORD": "[REDACTED]", "INVALID_EMAIL": "البريد الإلكتروني غير صالح", "INVALID_EMAIL_OR_PASSWORD": "[REDACTED]", "INVALID_USER": "مستخدم غير صالح", "SOCIAL_ACCOUNT_ALREADY_LINKED": "الحساب الاجتماعي مرتبط بالفعل", "PROVIDER_NOT_FOUND": "مزود الخدمة غير موجود", "INVALID_TOKEN": "[REDACTED]", "TOKEN_EXPIRED": "انتهت صلاحية الرمز", "ID_TOKEN_NOT_SUPPORTED": "الرمز المميز غير مدعوم", "FAILED_TO_GET_USER_INFO": "فشل في استرجاع معلومات المستخدم", "USER_EMAIL_NOT_FOUND": "البريد الإلكتروني للمستخدم غير موجود", "EMAIL_NOT_VERIFIED": "البريد الإلكتروني لم يتم التحقق منه", "PASSWORD_TOO_SHORT": "كلمة المرور قصيرة جدًا", "PASSWORD_TOO_LONG": "كلمة المرور طويلة جدًا", "USER_ALREADY_EXISTS": "المستخدم موجود مسبقًا", "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "البريد الإلكتروني موجود مسبقًا. استخدم بريدًا إلكترونيًا آخر.", "EMAIL_CAN_NOT_BE_UPDATED": "لا يمكن تحديث البريد الإلكتروني", "CREDENTIAL_ACCOUNT_NOT_FOUND": "حساب بيانات الاعتماد غير موجود", "SESSION_EXPIRED": "انتهت صلاحية الجلسة. يرجى تسجيل الدخول مجددًا لمتابعة هذا الإجراء.", "FAILED_TO_UNLINK_LAST_ACCOUNT": "لا يمكن إلغاء ربط الحساب الأخير", "ACCOUNT_NOT_FOUND": "الحساب غير موجود", "USER_ALREADY_HAS_PASSWORD": "[REDACTED]", "CROSS_SITE_NAVIGATION_LOGIN_BLOCKED": "تم حظر تسجيل الدخول عبر المواقع. يبدو أن هذا الطلب هجوم CSRF.", "VERIFICATION_EMAIL_NOT_ENABLED": "بريد التحقق غير مفعّل", "EMAIL_ALREADY_VERIFIED": "البريد الإلكتروني تم التحقق منه بالفعل", "EMAIL_MISMATCH": "البريد الإلكتروني غير متطابق", "SESSION_NOT_FRESH": "الجلسة ليست حديثة", "LINKED_ACCOUNT_ALREADY_EXISTS": "الحساب المرتبط موجود بالفعل", "INVALID_ORIGIN": "مصدر غير صالح", "INVALID_CALLBACK_URL": "رابط إعادة التوجيه غير صالح", "INVALID_REDIRECT_URL": "رابط إعادة التوجيه غير صالح", "INVALID_ERROR_CALLBACK_URL": "رابط خطأ إعادة التوجيه غير صالح", "INVALID_NEW_USER_CALLBACK_URL": "رابط المستخدم الجديد غير صالح", "MISSING_OR_NULL_ORIGIN": "المصدر مفقود أو فارغ", "CALLBACK_URL_REQUIRED": "رابط إعادة التوجيه مطلوب", "FAILED_TO_CREATE_VERIFICATION": "تعذّر إنشاء عملية التحقق", "FIELD_NOT_ALLOWED": "هذا الحقل غير مسموح بتعيينه", "VALIDATION_ERROR": "خطأ في التحقق", "MISSING_FIELD": "هذا الحقل مطلوب", "PASSWORD_ALREADY_SET": "المستخدم لديه كلمة مرور مسبقًا", "INVALID_USERNAME_OR_PASSWORD": "[REDACTED]", "USERNAME_IS_ALREADY_TAKEN": "اسم المستخدم مأخوذ بالفعل. الرجاء اختيار اسم آخر.", "USERNAME_TOO_SHORT": "اسم المستخدم قصير جدًا", "USERNAME_TOO_LONG": "اسم المستخدم طويل جدًا", "INVALID_USERNAME": "اسم المستخدم غير صالح", "INVALID_DISPLAY_USERNAME": "اسم العرض غير صالح", "YOU_CANNOT_BAN_YOURSELF": "لا يمكنك حظر نفسك", "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "غير مصرّح لك بتغيير أدوار المستخدمين", "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "غير مصرّح لك بإنشاء مستخدمين", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "غير مصرّح لك بعرض قائمة المستخدمين", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "غير مصرّح لك بعرض جلسات المستخدمين", "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "غير مصرّح لك بحظر المستخدمين", "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "غير مصرّح لك بانتحال هوية المستخدمين", "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "غير مصرّح لك بإلغاء جلسات المستخدمين", "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "غير مصرّح لك بحذف المستخدمين", "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]", "BANNED_USER": "تم حظرك من هذا التطبيق", "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "غير مصرّح لك بعرض المستخدم", "NO_DATA_TO_UPDATE": "لا توجد بيانات للتحديث", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "غير مصرّح لك بتحديث المستخدمين", "YOU_CANNOT_REMOVE_YOURSELF": "لا يمكنك إزالة نفسك", "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE": "غير مصرّح لك بتعيين قيمة دور غير موجودة", "YOU_CANNOT_IMPERSONATE_ADMINS": "لا يمكنك انتحال هوية المشرفين", "INVALID_ROLE_TYPE": "نوع الدور غير صالح" } } } } }, { "name": "next-cookies", "config": { "id": "next-cookies", "hooks": { "before": [ {} ], "after": [ {} ] } } } ], "advanced": { "database": { "generateId": false } } } } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { PrismaClient } from "@/app/generated/prisma/client"; import { PrismaPg } from "@prisma/adapter-pg"; import { betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; import { nextCookies } from "better-auth/next-js"; import { admin, username } from "better-auth/plugins"; import "dotenv/config"; import { authCustomConfig } from "./auth-config"; const prismaPostgresAdapter = new PrismaPg({ connectionString: process.env.DATABASE_URL, }); const prisma = new PrismaClient({ adapter: prismaPostgresAdapter, }); export const auth = betterAuth({ database: prismaAdapter(prisma, { provider: "postgresql", }), emailAndPassword: { enabled: true, }, plugins: [ username(), admin({ ...authCustomConfig, defaultRole: "USER", }), nextCookies(), // keep this plugin last ], advanced: { database: { generateId: false, }, }, }); ``` ### Additional context ### Environment - better-auth: 1.5.4 - @better-auth/i18n: 1.5.4 - Plugins: username, admin, i18n ### Describe the bug The `@better-auth/i18n` plugin does not translate error messages thrown from plugin **before hooks**. Only errors thrown from **endpoint handlers** are translated. For example, during `/sign-up/email`: - `USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL` — thrown from the endpoint handler → **translated** ✅ - `USERNAME_IS_ALREADY_TAKEN` — thrown from the username plugin's before hook → **not translated** ❌ // Translated (endpoint error): [APIError] { code: 'USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL', message: 'البريد الإلكتروني موجود مسبقًا. استخدم بريدًا إلكترونيًا آخر.', originalMessage: 'User already exists. Use another email.' } // NOT translated (before hook error): [APIError] { code: 'USERNAME_IS_ALREADY_TAKEN', message: 'Username is already taken. Please try another.' // no originalMessage, no translation applied } ### Root cause The i18n plugin registers only an **after hook** (`hooks: { after: [...] }`). In `to-auth-endpoints.ts`, the execution flow is: 1. `runBeforeHooks()` — username plugin's before hook throws `APIError` 2. The error propagates immediately — **`runAfterHooks()` never executes** 3. The i18n after-hook never gets a chance to translate Compare with endpoint errors: they are caught (lines 58-69 in `to-auth-endpoints.mjs`), stored in `context.returned`, and then `runAfterHooks()` runs — so i18n can translate them. ### Affected error codes All errors thrown from the username plugin's before hook (lines 188-217 in `dist/plugins/username/index.mjs`) on `/sign-up/email` and `/update-user`: - `USERNAME_IS_ALREADY_TAKEN` - `USERNAME_TOO_SHORT` - `USERNAME_TOO_LONG` - `INVALID_USERNAME` - `INVALID_DISPLAY_USERNAME` This likely affects **any plugin** that throws `APIError` from a before hook, not just the username plugin. ### Expected behavior All error codes should be translated by the i18n plugin regardless of whether they originate from a before hook, endpoint handler, or after hook. ### Possible fix In `runBeforeHooks()` (in `to-auth-endpoints.ts`), before-hook `APIError`s should be caught and routed through `runAfterHooks()` the same way endpoint errors are, so the i18n plugin (and any other after-hook-based plugin) can process them. ### Additional context - This is the same class of bug as #7969 (captcha errors not translated), which was closed with a captcha-specific fix rather than addressing the underlying before-hook bypass. - The third-party package [better-auth-localization](https://github.com/marcellosso/better-auth-localization) also uses after hooks and has the same limitation — the username plugin's `USERNAME_IS_ALREADY_TAKEN` translation exists in their language files but can never actually be applied at runtime.
GiteaMirror added the devtoolscredentialsbug labels 2026-04-15 19:03:59 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Mar 8, 2026):

Hi @samerkh! I'm Dosu and I’m helping the better-auth team.

Your analysis is spot-on. I confirmed this is a known architectural limitation in better-auth's hook system.

Root cause confirmed: The i18n plugin only registers an after hook, but when runBeforeHooks() throws an error, execution terminates immediately without ever reaching runAfterHooks().

The username plugin throws USERNAME_IS_ALREADY_TAKEN in its before hook, while USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL is thrown in the endpoint handler — explaining exactly why only one gets translated.

Related open issue: #7969 reports the same problem with captcha errors not being translated, and it remains unresolved.

Your suggested fix (catching before-hook errors and routing them through runAfterHooks()) aligns with what's needed architecturally.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:4018340749 --> @dosubot[bot] commented on GitHub (Mar 8, 2026): <!-- Greeting --> Hi @samerkh! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> Your analysis is spot-on. I confirmed this is a known architectural limitation in better-auth's hook system. **Root cause confirmed:** The [i18n plugin only registers an after hook](https://github.com/better-auth/better-auth/blob/8e0e53ed9b8918f43dd8e60194580d1357f9e0fb/packages/i18n/src/index.ts#L154-L185), but when [`runBeforeHooks()` throws an error](https://github.com/better-auth/better-auth/blob/8e0e53ed9b8918f43dd8e60194580d1357f9e0fb/packages/better-auth/src/api/to-auth-endpoints.ts#L234-L243), execution terminates immediately without ever reaching `runAfterHooks()`. The [username plugin throws `USERNAME_IS_ALREADY_TAKEN` in its before hook](https://github.com/better-auth/better-auth/blob/8e0e53ed9b8918f43dd8e60194580d1357f9e0fb/packages/better-auth/src/plugins/username/index.ts#L545-L548), while [`USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL` is thrown in the endpoint handler](https://github.com/better-auth/better-auth/blob/8e0e53ed9b8918f43dd8e60194580d1357f9e0fb/packages/better-auth/src/api/routes/sign-up.ts#L311-L314) — explaining exactly why only one gets translated. **Related open issue:** [#7969](https://github.com/better-auth/better-auth/issues/7969) reports the same problem with captcha errors not being translated, and it remains unresolved. Your suggested fix (catching before-hook errors and routing them through `runAfterHooks()`) aligns with what's needed architecturally. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/fe73492d-816e-4550-9eef-a6bb293190da?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/fe73492d-816e-4550-9eef-a6bb293190da?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/fe73492d-816e-4550-9eef-a6bb293190da?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/fe73492d-816e-4550-9eef-a6bb293190da?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/fe73492d-816e-4550-9eef-a6bb293190da?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/fe73492d-816e-4550-9eef-a6bb293190da?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/fe73492d-816e-4550-9eef-a6bb293190da?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=Your%20analysis%20is%20spot-on.%20I%20confirmed%20this%20is%20a%20known%20architectural%20limitation%20in%20better-auth%27s%20hook%20system.%0A%0A%2A%2ARoot%20cause%20confirmed%3A%2A%2A%20The%20%5Bi18n%20plugin%20only%20registers%20an%20after%20hook%5D%28https%3A//github.com/better-auth/better-auth/blob/8e0e53ed9b8918f43dd8e60194580d1357f9e0fb/packages/i18n/src/index.ts%23L154-L185%29%2C%20but%20when%20%5B%60runBeforeHooks%28%29%60%20throws%20an%20error%5D%28https%3A//github.com/better-auth/better-auth/blob/8e0e53ed9b8918f43dd8e60194580d1357f9e0fb/packages/better-auth/src/api/to-auth-endpoints.ts%23L234-L243%29%2C%20execution%20terminates%20immediately%20without%20ever%20reaching%20%60runAfterHooks%28%29%60.%0A%0AThe%20%5Busername%20plugin%20throws%20%60USERNAME_IS_ALREADY_TAKEN%60%20in%20its%20before%20hook%5D%28https%3A//github.com/better-auth/better-auth/blob/8e0e53ed9b8918f43dd8e60194580d1357f9e0fb/packages/better-auth/src/plugins/username/index.ts%23L545-L548%29%2C%20while%20%5B%60USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL%60%20is%20thrown%20in%20the%20endpoint%20handler%5D%28https%3A//github.com/better-auth/better-auth/blob/8e0e53ed9b8918f43dd8e60194580d1357f9e0fb/packages/better-auth/src/api/routes/sign-up.ts%23L311-L314%29%20%E2%80%94%20explaining%20exactly%20why%20only%20one%20gets%20translated.%0A%0A%2A%2ARelated%20open%20issue%3A%2A%2A%20%5B%237969%5D%28https%3A//github.com/better-auth/better-auth/issues/7969%29%20reports%20the%20same%20problem%20with%20captcha%20errors%20not%20being%20translated%2C%20and%20it%20remains%20unresolved.%0A%0AYour%20suggested%20fix%20%28catching%20before-hook%20errors%20and%20routing%20them%20through%20%60runAfterHooks%28%29%60%29%20aligns%20with%20what%27s%20needed%20architecturally.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/8492)
Author
Owner

@OnlyTerp commented on GitHub (Mar 8, 2026):

I'd like to work on this issue. I've analyzed the codebase and have a fix ready.

<!-- gh-comment-id:4018352727 --> @OnlyTerp commented on GitHub (Mar 8, 2026): I'd like to work on this issue. I've analyzed the codebase and have a fix ready.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#19735