import { sendRedirect } from 'h3'; import type { LazyDatabase } from '..'; import locales from '~~/locale/locales.ts'; import type { LocaleDescription } from '~~/locale/locales.ts'; import { getLocale, loadConfig } from '~~/server/data.ts'; /** * When the current locale represents a macrolanguage, redirects the user to the * appropriate sublanguage based on the Accept-Language header. */ export default defineEventHandler(async (event) => { const { node: { req }, path } = event; const locale = getLocale(event); const config = await loadConfig(locale); if ( !config.macrolanguage?.enabled || /(?:langswitch|__nuxt_error)(?:\/|#|\?|$)/.test(path) || path.startsWith('/api/') ) { return; } const langswitchPath = path !== '/' ? `/langswitch?return=${encodeURIComponent(path)}` : '/langswitch'; const acceptLanguage = req.headers['accept-language']; // Map individual languages to their locales const individualLanguages = config.macrolanguage?.languages || []; const subLocales = new Map( // @ts-expect-error I don't have a clue why this is bugging out individualLanguages .map((subLanguage) => { const foundLocale = locales.find( (locale) => locale.code === subLanguage.code, )!; return foundLocale ? [subLanguage.code, foundLocale] : null; }) .filter( // @ts-expect-error TypeScript is confused about the type here (locale): locale is [string, LocaleDescription][] => locale !== null, ), ); // Only redirect to individual locale automatically if all individual languages // have some language codes to match against the Accept-Language header const canAutoRedirect = individualLanguages.every( (subLanguage) => subLanguage.matches?.some(Boolean), ); if (!acceptLanguage || !individualLanguages || !canAutoRedirect) { return sendRedirect(event, langswitchPath); } // Get language codes from the Accept-Language header, ordered by quality const requestedLangs = acceptLanguage.split(',').map((language) => { const [lang, quality] = language.split(';q='); return { lang, quality: quality ? Number.parseFloat(quality) : 1 }; }) .sort((a, b) => b.quality - a.quality) .map((acceptEntry) => acceptEntry.lang); // If the requested route is for a user profile, get which individual // languages the user has a profile for if (path.startsWith('/@') && event.context.db) { const db = event.context.db as LazyDatabase; const username = /^\/@([^/?#]+)/.exec(path)?.[1]; if (!username) { return sendRedirect(event, langswitchPath); } const profiles = (await db.all<{ id: string; locale: string; username: string }>(` SELECT profiles.id, profiles.locale, users.username FROM profiles LEFT JOIN users on profiles.userId = users.id WHERE users.username = ? AND profiles.locale IN (${ individualLanguages.map(() => '?').join(', ') }) `, [username, ...individualLanguages.map((lang) => lang.code)])); if (profiles.length !== 1) { return sendRedirect(event, langswitchPath); } const foundLocale = subLocales.get(profiles[0].locale); if (foundLocale) { return sendRedirect(event, `${foundLocale.url}${req.url}`); } return sendRedirect(event, langswitchPath); } // Find the first locale matching an individual language of the // macrolanguage let localeDescription: LocaleDescription | undefined; // Iterate in order of the Accept-Language header's quality values, to // prioritize the user's preferred languages for (const tag of requestedLangs) { const languageConfig = individualLanguages.find( (lang) => lang.matches?.includes(tag), ); if (!languageConfig) { break; } const foundLocaleDescription = subLocales.get(languageConfig.code); if (foundLocaleDescription) { localeDescription = foundLocaleDescription; break; } } if (!localeDescription) { return sendRedirect(event, langswitchPath); } return sendRedirect(event, `${localeDescription.url}${req.url}`); });