PronounsPage/server/middleware/macrolanguage-redirect.ts
Adaline Simonian 979aa097c7
feat: macrolanguage landing page for no locale
Auto-redirects:
- if the page requested is a user profile, and the user has one profile
  out of the multiple individual languages, to the one profile that was
  found
- otherwise based on user's accept-language header
- if all else fails, shows language selection landing page
2025-01-15 16:18:01 -08:00

131 lines
4.2 KiB
TypeScript

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