209 lines
6.2 KiB
Plaintext
209 lines
6.2 KiB
Plaintext
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
const require_utils_accept = require("../../utils/accept.js");
|
|
const require_helper_cookie_index = require("../../helper/cookie/index.js");
|
|
//#region src/middleware/language/language.ts
|
|
const DEFAULT_OPTIONS = {
|
|
order: [
|
|
"querystring",
|
|
"cookie",
|
|
"header"
|
|
],
|
|
lookupQueryString: "lang",
|
|
lookupCookie: "language",
|
|
lookupFromHeaderKey: "accept-language",
|
|
lookupFromPathIndex: 0,
|
|
caches: ["cookie"],
|
|
ignoreCase: true,
|
|
fallbackLanguage: "en",
|
|
supportedLanguages: ["en"],
|
|
cookieOptions: {
|
|
sameSite: "Strict",
|
|
secure: true,
|
|
maxAge: 31536e3,
|
|
httpOnly: true
|
|
},
|
|
debug: false
|
|
};
|
|
/**
|
|
* Parse Accept-Language header values with quality scores
|
|
* @param header Accept-Language header string
|
|
* @returns Array of parsed languages with quality scores
|
|
*/
|
|
function parseAcceptLanguage(header) {
|
|
return require_utils_accept.parseAccept(header).map(({ type, q }) => ({
|
|
lang: type,
|
|
q
|
|
}));
|
|
}
|
|
/**
|
|
* Validate and normalize language codes
|
|
* @param lang Language code to normalize
|
|
* @param options Detector options
|
|
* @returns Normalized language code or undefined
|
|
*/
|
|
const normalizeLanguage = (lang, options) => {
|
|
if (!lang) return;
|
|
try {
|
|
let normalizedLang = lang.trim();
|
|
if (options.convertDetectedLanguage) normalizedLang = options.convertDetectedLanguage(normalizedLang);
|
|
const compLang = options.ignoreCase ? normalizedLang.toLowerCase() : normalizedLang;
|
|
const compSupported = options.supportedLanguages.map((l) => options.ignoreCase ? l.toLowerCase() : l);
|
|
const exactIndex = compSupported.indexOf(compLang);
|
|
if (exactIndex !== -1) return options.supportedLanguages[exactIndex];
|
|
let longestMatchIndex = -1;
|
|
let longestMatchLength = -1;
|
|
for (let i = 0; i < compSupported.length; i++) {
|
|
const candidate = compSupported[i];
|
|
if (candidate.length < compLang.length && candidate.length > longestMatchLength && compLang.startsWith(candidate) && compLang[candidate.length] === "-") {
|
|
longestMatchIndex = i;
|
|
longestMatchLength = candidate.length;
|
|
}
|
|
}
|
|
if (longestMatchIndex !== -1) return options.supportedLanguages[longestMatchIndex];
|
|
return;
|
|
} catch {
|
|
return;
|
|
}
|
|
};
|
|
/**
|
|
* Detects language from query parameter
|
|
*/
|
|
const detectFromQuery = (c, options) => {
|
|
const query = c.req.query(options.lookupQueryString);
|
|
return normalizeLanguage(query, options);
|
|
};
|
|
/**
|
|
* Detects language from cookie
|
|
*/
|
|
const detectFromCookie = (c, options) => {
|
|
const cookie = require_helper_cookie_index.getCookie(c, options.lookupCookie);
|
|
return normalizeLanguage(cookie, options);
|
|
};
|
|
/**
|
|
* Detects language from Accept-Language header
|
|
*/
|
|
function detectFromHeader(c, options) {
|
|
try {
|
|
const acceptLanguage = c.req.header(options.lookupFromHeaderKey);
|
|
if (!acceptLanguage) return;
|
|
const languages = parseAcceptLanguage(acceptLanguage);
|
|
for (const { lang, q } of languages) {
|
|
if (q === 0) continue;
|
|
const normalizedLang = normalizeLanguage(lang, options);
|
|
if (normalizedLang) return normalizedLang;
|
|
}
|
|
return;
|
|
} catch {
|
|
return;
|
|
}
|
|
}
|
|
/**
|
|
* Detects language from URL path
|
|
*/
|
|
function detectFromPath(c, options) {
|
|
const langSegment = new URL(c.req.url).pathname.split("/").filter(Boolean)[options.lookupFromPathIndex];
|
|
return normalizeLanguage(langSegment, options);
|
|
}
|
|
/**
|
|
* Collection of all language detection strategies
|
|
*/
|
|
const detectors = {
|
|
querystring: detectFromQuery,
|
|
cookie: detectFromCookie,
|
|
header: detectFromHeader,
|
|
path: detectFromPath
|
|
};
|
|
/**
|
|
* Validate detector options
|
|
* @param options Detector options to validate
|
|
* @throws Error if options are invalid
|
|
*/
|
|
function validateOptions(options) {
|
|
if (!options.supportedLanguages.includes(options.fallbackLanguage)) throw new Error("Fallback language must be included in supported languages");
|
|
if (options.lookupFromPathIndex < 0) throw new Error("Path index must be non-negative");
|
|
if (!options.order.every((detector) => Object.keys(detectors).includes(detector))) throw new Error("Invalid detector type in order array");
|
|
}
|
|
/**
|
|
* Cache detected language
|
|
*/
|
|
function cacheLanguage(c, language, options) {
|
|
if (!Array.isArray(options.caches) || !options.caches.includes("cookie")) return;
|
|
try {
|
|
require_helper_cookie_index.setCookie(c, options.lookupCookie, language, options.cookieOptions);
|
|
} catch (error) {
|
|
if (options.debug) console.error("Failed to cache language:", error);
|
|
}
|
|
}
|
|
/**
|
|
* Detect language from request
|
|
*/
|
|
const detectLanguage = (c, options) => {
|
|
let detectedLang;
|
|
for (const detectorName of options.order) {
|
|
const detector = detectors[detectorName];
|
|
try {
|
|
detectedLang = detector(c, options);
|
|
if (detectedLang) {
|
|
if (options.debug) console.log(`Language detected from ${detectorName}: ${detectedLang}`);
|
|
break;
|
|
}
|
|
} catch (error) {
|
|
if (options.debug) console.error(`Error in ${detectorName} detector:`, error);
|
|
continue;
|
|
}
|
|
}
|
|
const finalLang = detectedLang || options.fallbackLanguage;
|
|
if (detectedLang && options.caches) cacheLanguage(c, finalLang, options);
|
|
return finalLang;
|
|
};
|
|
/**
|
|
* Language detector middleware factory
|
|
* @param userOptions Configuration options for the language detector
|
|
* @returns Hono middleware function
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* type Variables = LanguageVariables
|
|
* const app = new Hono<{ Variables: Variables }>()
|
|
*
|
|
* app.use(
|
|
* languageDetector({
|
|
* supportedLanguages: ['en', 'ja'], // Must include fallback
|
|
* fallbackLanguage: 'en', // Required
|
|
* })
|
|
* )
|
|
*
|
|
* app.get('/', (c) => {
|
|
* const lang = c.get('language')
|
|
* return c.text(`Current language: ${lang}`)
|
|
* })
|
|
* ```
|
|
*/
|
|
const languageDetector = (userOptions) => {
|
|
const options = {
|
|
...DEFAULT_OPTIONS,
|
|
...userOptions,
|
|
cookieOptions: {
|
|
...DEFAULT_OPTIONS.cookieOptions,
|
|
...userOptions.cookieOptions
|
|
}
|
|
};
|
|
validateOptions(options);
|
|
return async function languageDetector(ctx, next) {
|
|
const lang = detectLanguage(ctx, options);
|
|
ctx.set("language", lang);
|
|
await next();
|
|
};
|
|
};
|
|
//#endregion
|
|
exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
|
|
exports.detectFromCookie = detectFromCookie;
|
|
exports.detectFromHeader = detectFromHeader;
|
|
exports.detectFromPath = detectFromPath;
|
|
exports.detectFromQuery = detectFromQuery;
|
|
exports.detectors = detectors;
|
|
exports.languageDetector = languageDetector;
|
|
exports.normalizeLanguage = normalizeLanguage;
|
|
exports.parseAcceptLanguage = parseAcceptLanguage;
|
|
exports.validateOptions = validateOptions;
|