next-i18next
Version:
The easiest way to translate your NextJs apps.
173 lines (172 loc) • 7.44 kB
JavaScript
import { NextResponse } from "next/server";
//#region src/appRouter/config.ts
function defineConfig(config) {
return config;
}
function normalizeConfig(userConfig) {
const supportedLngs = userConfig.supportedLngs ?? userConfig.i18n?.locales?.filter((l) => l !== "default") ?? ["en"];
const fallbackLng = userConfig.fallbackLng ?? userConfig.i18n?.defaultLocale ?? supportedLngs[0];
if (!fallbackLng) throw new Error("next-i18next: fallbackLng (or i18n.defaultLocale) is required");
if (supportedLngs.length === 0) throw new Error("next-i18next: supportedLngs (or i18n.locales) must contain at least one language");
const defaultNS = userConfig.defaultNS ?? "common";
return {
supportedLngs,
fallbackLng,
defaultNS,
ns: userConfig.ns ?? [defaultNS],
localeInPath: userConfig.localeInPath ?? true,
hideDefaultLocale: userConfig.hideDefaultLocale ?? false,
localePath: userConfig.localePath ?? "/locales",
localeStructure: userConfig.localeStructure ?? "{{lng}}/{{ns}}",
localeExtension: userConfig.localeExtension ?? "json",
cookieName: userConfig.cookieName ?? "i18next",
headerName: userConfig.headerName ?? "x-i18next-current-language",
cookieMaxAge: userConfig.cookieMaxAge ?? 365 * 24 * 60 * 60,
ignoredPaths: userConfig.ignoredPaths ?? [
"/api",
"/_next",
"/static"
],
basePath: userConfig.basePath,
resources: userConfig.resources,
resourceLoader: userConfig.resourceLoader,
use: userConfig.use ?? [],
i18nextOptions: userConfig.i18nextOptions ?? {},
nonExplicitSupportedLngs: userConfig.nonExplicitSupportedLngs ?? false,
i18n: userConfig.i18n,
serializeConfig: userConfig.serializeConfig,
reloadOnPrerender: userConfig.reloadOnPrerender
};
}
//#endregion
//#region src/appRouter/proxy/languageDetector.ts
/**
* Edge-safe Accept-Language parser and language matcher.
* No external dependencies — runs in Edge Runtime, Node.js, and browser.
*/
function parseAcceptLanguage(header) {
if (!header) return [];
return header.split(",").map((part) => {
const [lang, qPart] = part.trim().split(";");
const q = qPart?.trim().startsWith("q=") ? parseFloat(qPart.trim().slice(2)) : 1;
return {
lang: lang.trim(),
q: isNaN(q) ? 0 : q
};
}).filter((item) => item.lang && item.q > 0).sort((a, b) => b.q - a.q).map((item) => item.lang);
}
/**
* Find a matching supported language for a given code, respecting nonExplicitSupportedLngs.
*
* Matching order (mirrors i18next's LanguageUtils.getBestMatchFromCodes):
* 1. Exact match (case-insensitive)
* 2. Preferred prefix → supported base (e.g. preferred 'en-US' matches supported 'en')
* 3. If nonExplicitSupportedLngs: preferred base → supported region
* (e.g. preferred 'en' matches supported 'en-US')
*/
function findSupportedMatch(code, supportedLanguages, nonExplicitSupportedLngs) {
const lower = code.toLowerCase();
const exact = supportedLanguages.find((l) => l.toLowerCase() === lower);
if (exact) return exact;
const prefix = lower.split("-")[0];
const partial = supportedLanguages.find((l) => l.toLowerCase() === prefix);
if (partial) return partial;
if (nonExplicitSupportedLngs) {
const reverse = supportedLanguages.find((l) => l.toLowerCase().split("-")[0] === prefix);
if (reverse) return reverse;
}
}
function matchLanguage(acceptLanguages, supportedLanguages, defaultLanguage, nonExplicitSupportedLngs = false) {
for (const preferred of acceptLanguages) {
const match = findSupportedMatch(preferred, supportedLanguages, nonExplicitSupportedLngs);
if (match) return match;
}
return defaultLanguage;
}
//#endregion
//#region src/appRouter/proxy/index.ts
function findLocaleInPath(pathname, supportedLngs, nonExplicitSupportedLngs) {
const match = pathname.match(/^\/([^/]+)/);
if (!match) return void 0;
return findSupportedMatch(match[1], supportedLngs, nonExplicitSupportedLngs);
}
function createProxy(userConfig) {
const config = normalizeConfig(userConfig);
const nonExplicit = config.nonExplicitSupportedLngs;
const basePath = config.basePath ? "/" + config.basePath.replace(/^\/+/, "").replace(/\/+$/, "") : void 0;
return function middleware(req) {
const { pathname, search } = req.nextUrl;
if (basePath) {
if (pathname !== basePath && !pathname.startsWith(basePath + "/")) return NextResponse.next();
}
for (const ignored of config.ignoredPaths) if (pathname.startsWith(ignored)) return NextResponse.next();
if (/\.(ico|png|jpg|jpeg|svg|gif|webp|css|js|map|woff2?|ttf|eot)$/.test(pathname)) return NextResponse.next();
let lng;
const cookieValue = req.cookies.get(config.cookieName)?.value;
if (cookieValue) lng = matchLanguage([cookieValue], config.supportedLngs, config.fallbackLng, nonExplicit);
if (!lng) lng = matchLanguage(parseAcceptLanguage(req.headers.get("Accept-Language")), config.supportedLngs, config.fallbackLng, nonExplicit);
if (!lng) lng = config.fallbackLng;
const lngInPath = findLocaleInPath(basePath ? pathname.slice(basePath.length) || "/" : pathname, config.supportedLngs, nonExplicit);
if (config.localeInPath) {
const prefix = basePath ?? "";
const pathAfterBase = basePath ? pathname.slice(basePath.length) : pathname;
if (config.hideDefaultLocale && lngInPath === config.fallbackLng) {
const pathWithoutLocale = pathAfterBase.replace(/^\/[^/]+/, "") || "/";
const redirectUrl = new URL(`${prefix}${pathWithoutLocale}${search}`, req.url);
const response = NextResponse.redirect(redirectUrl);
response.cookies.set(config.cookieName, config.fallbackLng, {
path: "/",
maxAge: config.cookieMaxAge,
sameSite: "lax"
});
return response;
}
const headers = new Headers(req.headers);
headers.set(config.headerName, lngInPath || lng);
if (!lngInPath) {
if (config.hideDefaultLocale) {
const rewriteUrl = new URL(`${prefix}/${config.fallbackLng}${pathAfterBase}${search}`, req.url);
headers.set(config.headerName, config.fallbackLng);
const response = NextResponse.rewrite(rewriteUrl, { request: { headers } });
response.cookies.set(config.cookieName, config.fallbackLng, {
path: "/",
maxAge: config.cookieMaxAge,
sameSite: "lax"
});
return response;
}
const redirectUrl = new URL(`${prefix}/${lng}${pathAfterBase}${search}`, req.url);
const response = NextResponse.redirect(redirectUrl);
response.cookies.set(config.cookieName, lng, {
path: "/",
maxAge: config.cookieMaxAge,
sameSite: "lax"
});
return response;
}
const response = NextResponse.next({ request: { headers } });
if (req.headers.has("referer")) {
const refererUrl = new URL(req.headers.get("referer"));
const lngInReferer = findLocaleInPath(basePath ? refererUrl.pathname.slice(basePath.length) || "/" : refererUrl.pathname, config.supportedLngs, nonExplicit);
if (lngInReferer) response.cookies.set(config.cookieName, lngInReferer, {
path: "/",
maxAge: config.cookieMaxAge,
sameSite: "lax"
});
}
return response;
} else {
const headers = new Headers(req.headers);
headers.set(config.headerName, lng);
return NextResponse.next({ request: { headers } });
}
};
}
/**
* Backwards-compatible alias for createProxy.
* Use `createProxy` for new projects with Next.js 16+ `proxy.ts`.
*/
const createMiddleware = createProxy;
//#endregion
export { createMiddleware, createProxy, defineConfig, normalizeConfig };
//# sourceMappingURL=index.mjs.map