PronounsPage/server/middleware/macrolanguage-redirect.ts

130 lines
4.5 KiB
TypeScript

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<string, LocaleDescription>(
// @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}`);
});