UNPKG

@tanosugi/better-auth-utils

Version:

Better Auth Utils

276 lines (275 loc) 9.86 kB
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, }), }; }, }; };