- H1: Add missing sanitize() to help center content rendering - H2: Tighten CSP by removing unsafe-inline/unsafe-eval from script-src; move theme init to external JS file with meta tag for defaultDark - M1: Add SSRF protection for radio API URLs (block private IPs) - M2: Add rate limiting to SSO ticket endpoint (5 req/30s per user) - M4: Document locale validation safety in i18n dynamic import - L1: Truncate stacktraces in admin commandocentrum to first 20 lines
47 lines
2.0 KiB
TypeScript
47 lines
2.0 KiB
TypeScript
import { cookies } from "next/headers";
|
|
import { getRequestConfig } from "next-intl/server";
|
|
|
|
// i18n WITHOUT routing: the locale is chosen by a `NEXT_LOCALE` cookie (set by
|
|
// the language switcher) rather than a URL segment, so every existing route and
|
|
// the access-guard middleware keep working unchanged. English is the fallback.
|
|
export const SUPPORTED_LOCALES = ["en", "it", "nl", "de", "fr", "es"] as const;
|
|
export type AppLocale = (typeof SUPPORTED_LOCALES)[number];
|
|
export const DEFAULT_LOCALE: AppLocale = "en";
|
|
|
|
export function isSupportedLocale(value: string | undefined): value is AppLocale {
|
|
return !!value && (SUPPORTED_LOCALES as readonly string[]).includes(value);
|
|
}
|
|
|
|
export default getRequestConfig(async () => {
|
|
const store = await cookies();
|
|
const cookieLocale = store.get("NEXT_LOCALE")?.value;
|
|
const locale: AppLocale = isSupportedLocale(cookieLocale) ? cookieLocale : DEFAULT_LOCALE;
|
|
|
|
// Safe: `locale` is validated against SUPPORTED_LOCALES above (only en/it/nl/de/fr/es).
|
|
const messages = (await import(`../messages/${locale}.json`)).default;
|
|
// English is the source of truth; fall back to it for any key missing from a
|
|
// translation so the UI never shows a raw key path.
|
|
const fallback =
|
|
locale === DEFAULT_LOCALE
|
|
? messages
|
|
: (await import(`../messages/${DEFAULT_LOCALE}.json`)).default;
|
|
|
|
return {
|
|
locale,
|
|
messages,
|
|
// Never throw on a missing message — use the English value, or a humanised
|
|
// key as a last resort.
|
|
getMessageFallback({ key }: { key: string }) {
|
|
const fromEnglish = key
|
|
.split(".")
|
|
.reduce<unknown>((o, k) => (o && typeof o === "object" ? (o as Record<string, unknown>)[k] : undefined), fallback);
|
|
if (typeof fromEnglish === "string") return fromEnglish;
|
|
const seg = key.split(".").pop() ?? key;
|
|
return seg.replace(/[._-]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
},
|
|
onError() {
|
|
// Swallow MISSING_MESSAGE etc. — getMessageFallback already degrades.
|
|
},
|
|
};
|
|
});
|