import fs from 'fs'; import { sendRedirect } from 'h3'; import SUML from 'suml'; import type { LazyDatabase } from '..'; import type { Config } from '~/locale/config.ts'; import locales from '~/locale/locales.ts'; import type { LocaleDescription } from '~/locale/locales.ts'; const config = new SUML() .parse(fs.readFileSync('./data/config.suml', 'utf-8')) as Config; // Map individual languages to their locales const individualLanguages = config.macrolanguage?.languages || []; const subLocales = new Map( individualLanguages .map((subLanguage) => { const foundLocale = locales.find( (locale) => locale.code === subLanguage.code, ); return foundLocale ? [subLanguage.code, foundLocale] : null; }) .filter( (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), ); /** * 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; 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']; 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 locale: 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 foundLocale = subLocales.get(languageConfig.code); if (foundLocale) { locale = foundLocale; break; } } if (!locale) { return sendRedirect(event, langswitchPath); } return sendRedirect(event, `${locale.url}${req.url}`); });