@tanosugi/better-auth-utils
Version:
Better Auth Utils
276 lines (275 loc) • 9.86 kB
JavaScript
import arDict from "../../locales/error-codes/ar.json";
import csDict from "../../locales/error-codes/cs.json";
import deDict from "../../locales/error-codes/de.json";
import enDict from "../../locales/error-codes/en.json";
import esDict from "../../locales/error-codes/es.json";
import frDict from "../../locales/error-codes/fr.json";
import hiDict from "../../locales/error-codes/hi.json";
import idDict from "../../locales/error-codes/id.json";
import itDict from "../../locales/error-codes/it.json";
import jaDict from "../../locales/error-codes/ja.json";
import koDict from "../../locales/error-codes/ko.json";
import nlDict from "../../locales/error-codes/nl.json";
import plDict from "../../locales/error-codes/pl.json";
import ptDict from "../../locales/error-codes/pt.json";
import ruDict from "../../locales/error-codes/ru.json";
import thDict from "../../locales/error-codes/th.json";
import trDict from "../../locales/error-codes/tr.json";
import ukDict from "../../locales/error-codes/uk.json";
import zhCNDict from "../../locales/error-codes/zh-CN.json";
import zhTWDict from "../../locales/error-codes/zh-TW.json";
// allowed locales
const allowedLocales = [
"en",
"ja",
"fr",
"de",
"es",
"it",
"nl",
"pl",
"pt",
"ru",
"th",
"tr",
"uk",
"zh-CN",
"zh-TW",
"ko",
"id",
"hi",
"cs",
"ar",
"all",
];
const allowedLocalesWithoutAll = allowedLocales.filter((l) => l !== "all");
// Function to determine language
/**
* getLang(ctx, options)
* Determines the language to use based on the request context and localization options.
* @param ctx - The request context (should have acceptLanguage and referer properties)
* @param options - Localization options (enabledLocales, i18nRouting, or singleLang)
* @returns {Locale | undefined} - The detected locale or undefined if not found
*
* Usage:
* const lang = getLang(ctx, options);
*/
export function getLang(ctx, options) {
const acceptLang = ctx.acceptLanguage;
const raw = acceptLang?.split(",")[0]?.trim().split(";")[0];
let firstLang;
if (raw === "zh-CN" || raw === "zh-TW") {
firstLang = raw;
}
else {
firstLang = raw?.split("-")[0];
}
if (options?.enabledLocales?.includes("all") ||
!(options?.enabledLocales || options?.i18nRouting || options?.singleLang)) {
if (firstLang &&
allowedLocalesWithoutAll.includes(firstLang)) {
return firstLang;
}
}
else if (options?.enabledLocales) {
if (firstLang && options.enabledLocales.includes(firstLang)) {
return firstLang;
}
}
else if (options?.i18nRouting) {
const url = ctx?.referer || "";
const path = url.replace(/^http?:\/\/[^/]+/, "");
const firstPath = path.split("/").filter(Boolean)[0];
if (firstPath && options.i18nRouting.includes(firstPath)) {
return firstPath;
}
}
else if (options?.singleLang) {
return options.singleLang;
}
return undefined;
}
// Dictionary mapping language code to dictionary
const localeDicts = {
en: enDict,
ja: jaDict,
fr: frDict,
de: deDict,
es: esDict,
it: itDict,
nl: nlDict,
pl: plDict,
pt: ptDict,
ru: ruDict,
th: thDict,
tr: trDict,
uk: ukDict,
"zh-CN": zhCNDict,
"zh-TW": zhTWDict,
ko: koDict,
id: idDict,
hi: hiDict,
cs: csDict,
ar: arDict,
};
// Function to return localized message from lang and code
/**
* getLocalizedMessage(lang, code)
* Returns the localized message string for the given language and code.
* @param lang - The language code (e.g., 'en', 'ja')
* @param code - The message code/key
* @returns {string | undefined} - The localized message or undefined if not found
*
* Usage:
* const message = getLocalizedMessage('en', 'LOGIN_SUCCESS');
*/
export function getLocalizedMessage(lang, code) {
const dict = lang && Object.prototype.hasOwnProperty.call(localeDicts, lang)
? localeDicts[lang]
: enDict;
if (!code)
return undefined;
return dict[code];
}
// tFuncServerとsimpleDictWithTFuncの組み合わせで辞書を生成する関数
export async function generateDictWithT(simpleDictWithTFunc, tFuncServer, locale) {
const t = await tFuncServer(locale);
return simpleDictWithTFunc(t);
}
// Main localization function
/**
* localization(options)
* Returns a BetterAuthPlugin for localization, handling request and response localization.
*
* @param {Object} options - Localization options object (optional)
* @param {Record<string, string>} [options.simpleDict] - Key-value pairs to merge into all languages.
* @param {Record<string, Partial<Record<Locale, string>>>} [options.multiLangDict] - Per-language
* message dictionary for each code.
* @param {Locale[]} [options.enabledLocales] - Array of allowed locales. "all" allows all supported locales.
* @param {Locale[]} [options.i18nRouting] - Locales to detect from the first path segment of the referer URL.
* @param {Exclude<Locale, "all">} [options.singleLang] - Always use this language for localization.
*
* @returns {BetterAuthPlugin} - The localization plugin object with onRequest and onResponse hooks.
*
* Usage:
* // Example 1: No arguments (default behavior)
* plugins: [localization()],
* // Uses Accept-Language header to determine the language and translate.
*
* // Example 2: singleLang
* plugins: [localization({
* singleLang: "ja"
* })],
* // Always uses Japanese for localization, regardless of request headers or URL.
*
* // Example 3: enabledLocales and multiLangDict (multiple languages)
* plugins: [localization({
* enabledLocales: ["en", "ja", "fr"],
* simpleDict: { LOGIN_SUCCESS: "Login successful!" },
* multiLangDict: {
* LOGIN_SUCCESS: {
* en: "Login successful!",
* ja: "ログイン成功!",
* fr: "Connexion réussie !"
* }
* }
* })],
* // The message will be translated based on Accept-Language header but limited to the enabledLocales.
* // simpleDict and multiLangDict are used with priority over default translations.
*
* // Example 4: i18nRouting
* plugins: [localization({
* i18nRouting: ["en", "ja"],
* simpleDict: {
* LOGIN_SUCCESS: "Login successful!"
* }
* })],
* // The language is determined from the first path segment of the referer URL.
*
* // Example 5: simpleDictWithTFunc and tFuncServer (for dynamic translation)
* plugins: [localization({
* i18nRouting: ["en", "ja"],
* simpleDictWithTFunc: (t) => ({ EMAIL_NOT_VERIFIED: t("Email not verified!!") }),
* tFuncServer: async (locale) => {
* const t = await getTranslations({ locale });
* return (key) => t(key);
* },
* })],
* // Used for i18n routing such as next-intl.
* // The message will be dynamically translated using the t function for the current locale.
*
* // Use plugin.onRequest and plugin.onResponse in your auth flow
*/
export const localization = (options) => {
// Generate merged dictionary from simpleDict and multiLangDict
const mergedDicts = {
...localeDicts,
};
if (options?.simpleDict) {
for (const lang of Object.keys(mergedDicts)) {
mergedDicts[lang] = { ...mergedDicts[lang], ...options.simpleDict };
}
}
if (options?.multiLangDict) {
for (const code of Object.keys(options.multiLangDict)) {
const langMap = options.multiLangDict[code];
if (langMap) {
for (const lang of Object.keys(langMap)) {
const val = langMap[lang];
if (val && mergedDicts[lang]) {
mergedDicts[lang][code] = val;
}
}
}
}
}
// 型定義で担保されているため、ここでのランタイムチェックは不要
return {
id: "localization",
async onRequest(req, ctx) {
ctx.acceptLanguage = req.headers.get("accept-language");
ctx.referer = req.headers.get("referer");
},
onResponse: async (response, ctx) => {
let body;
const contentType = response.headers?.get("content-type");
if (contentType?.includes("application/json")) {
try {
body = await response.json();
}
catch (e) {
body = {};
}
}
else {
body = {};
}
const lang = getLang(ctx, options);
let t = undefined;
let dictWithT = undefined;
if (options?.simpleDictWithTFunc && options?.tFuncServer) {
t = await options.tFuncServer(lang || "en");
dictWithT = options.simpleDictWithTFunc(t);
}
if (body?.code) {
// 優先度: simpleDictWithTFunc > multiLangDict/simpleDict > localeDicts
if (dictWithT && dictWithT[body.code] !== undefined) {
body.message = dictWithT[body.code];
}
else {
// multiLangDict/simpleDict/localeDicts
const dict = lang && mergedDicts[lang] ? mergedDicts[lang] : mergedDicts.en;
if (dict && dict[body.code] !== undefined) {
body.message = dict[body.code];
}
}
}
return {
response: new Response(JSON.stringify(body), {
status: response.status,
headers: response.headers,
}),
};
},
};
};