next-intl
Version:
Internationalization (i18n) for Next.js
191 lines (178 loc) • 7.21 kB
JavaScript
import { normalizeTrailingSlash, getSortedPathnames, matchesPathname, prefixPathname, getLocalePrefix, templateToRegex, getLocalizedTemplate } from '../shared/utils.js';
function getInternalTemplate(pathnames, pathname, locale) {
const sortedPathnames = getSortedPathnames(Object.keys(pathnames));
// Try to find a localized pathname that matches
for (const internalPathname of sortedPathnames) {
const localizedPathnamesOrPathname = pathnames[internalPathname];
if (typeof localizedPathnamesOrPathname === 'string') {
const localizedPathname = localizedPathnamesOrPathname;
if (matchesPathname(localizedPathname, pathname)) {
return [undefined, internalPathname];
}
} else {
// Prefer the entry with the current locale in case multiple
// localized pathnames match the current pathname
const sortedEntries = Object.entries(localizedPathnamesOrPathname);
const curLocaleIndex = sortedEntries.findIndex(([entryLocale]) => entryLocale === locale);
if (curLocaleIndex > 0) {
sortedEntries.unshift(sortedEntries.splice(curLocaleIndex, 1)[0]);
}
for (const [entryLocale] of sortedEntries) {
const localizedTemplate = getLocalizedTemplate(pathnames[internalPathname], entryLocale, internalPathname);
if (matchesPathname(localizedTemplate, pathname)) {
return [entryLocale, internalPathname];
}
}
}
}
// Try to find an internal pathname that matches (this can be the case
// if all localized pathnames are different from the internal pathnames)
for (const internalPathname of Object.keys(pathnames)) {
if (matchesPathname(internalPathname, pathname)) {
return [undefined, internalPathname];
}
}
// No match
return [undefined, undefined];
}
function formatTemplatePathname(sourcePathname, sourceTemplate, targetTemplate, prefix) {
const params = getRouteParams(sourceTemplate, sourcePathname);
let targetPathname = '';
targetPathname += formatPathnameTemplate(targetTemplate, params);
// A pathname with an optional catchall like `/categories/[[...slug]]`
// should be normalized to `/categories` if the catchall is not present
// and no trailing slash is configured
targetPathname = normalizeTrailingSlash(targetPathname);
return targetPathname;
}
/**
* Removes potential prefixes from the pathname.
*/
function getNormalizedPathname(pathname, locales, localePrefix) {
// Add trailing slash for consistent handling
// both for the root as well as nested paths
if (!pathname.endsWith('/')) {
pathname += '/';
}
const localePrefixes = getLocalePrefixes(locales, localePrefix);
const regex = new RegExp(`^(${localePrefixes.map(([, prefix]) => prefix.replaceAll('/', '\\/')).join('|')})/(.*)`, 'i');
const match = pathname.match(regex);
let result = match ? '/' + match[2] : pathname;
if (result !== '/') {
result = normalizeTrailingSlash(result);
}
return result;
}
function getLocalePrefixes(locales, localePrefix, sort = true) {
const prefixes = locales.map(locale => [locale, getLocalePrefix(locale, localePrefix)]);
if (sort) {
// More specific ones first
prefixes.sort((a, b) => b[1].length - a[1].length);
}
return prefixes;
}
function getPathnameMatch(pathname, locales, localePrefix, domain) {
const localePrefixes = getLocalePrefixes(locales, localePrefix);
// Sort to prioritize domain locales
if (domain) {
localePrefixes.sort(([localeA], [localeB]) => {
if (localeA === domain.defaultLocale) return -1;
if (localeB === domain.defaultLocale) return 1;
const isLocaleAInDomain = domain.locales.includes(localeA);
const isLocaleBInDomain = domain.locales.includes(localeB);
if (isLocaleAInDomain && !isLocaleBInDomain) return -1;
if (!isLocaleAInDomain && isLocaleBInDomain) return 1;
return 0;
});
}
for (const [locale, prefix] of localePrefixes) {
let exact, matches;
if (pathname === prefix || pathname.startsWith(prefix + '/')) {
exact = matches = true;
} else {
const normalizedPathname = pathname.toLowerCase();
const normalizedPrefix = prefix.toLowerCase();
if (normalizedPathname === normalizedPrefix || normalizedPathname.startsWith(normalizedPrefix + '/')) {
exact = false;
matches = true;
}
}
if (matches) {
return {
locale,
prefix,
matchedPrefix: pathname.slice(0, prefix.length),
exact
};
}
}
}
function getRouteParams(template, pathname) {
const normalizedPathname = normalizeTrailingSlash(pathname);
const normalizedTemplate = normalizeTrailingSlash(template);
const regex = templateToRegex(normalizedTemplate);
const match = regex.exec(normalizedPathname);
if (!match) return undefined;
const params = {};
for (let i = 1; i < match.length; i++) {
const key = normalizedTemplate.match(/\[([^\]]+)\]/g)?.[i - 1].replace(/[[\]]/g, '');
if (key) params[key] = match[i];
}
return params;
}
function formatPathnameTemplate(template, params) {
if (!params) return template;
// Simplify syntax for optional catchall ('[[...slug]]') so
// we can replace the value with simple interpolation
template = template.replace(/\[\[/g, '[').replace(/\]\]/g, ']');
let result = template;
Object.entries(params).forEach(([key, value]) => {
result = result.replace(`[${key}]`, value);
});
return result;
}
function formatPathname(pathname, prefix, search) {
let result = pathname;
if (prefix) {
result = prefixPathname(prefix, result);
}
if (search) {
result += search;
}
return result;
}
function getHost(requestHeaders) {
return requestHeaders.get('x-forwarded-host') ?? requestHeaders.get('host') ?? undefined;
}
function isLocaleSupportedOnDomain(locale, domain) {
return domain.defaultLocale === locale || domain.locales.includes(locale);
}
function getBestMatchingDomain(curHostDomain, locale, domainsConfig) {
let domainConfig;
// Prio 1: Stay on current domain
if (curHostDomain && isLocaleSupportedOnDomain(locale, curHostDomain)) {
domainConfig = curHostDomain;
}
// Prio 2: Use alternative domain with matching default locale
if (!domainConfig) {
domainConfig = domainsConfig.find(cur => cur.defaultLocale === locale);
}
// Prio 3: Use alternative domain that supports the locale
if (!domainConfig) {
domainConfig = domainsConfig.find(cur => cur.locales.includes(locale));
}
return domainConfig;
}
function applyBasePath(pathname, basePath) {
return normalizeTrailingSlash(basePath + pathname);
}
function getLocaleAsPrefix(locale) {
return `/${locale}`;
}
function sanitizePathname(pathname) {
// Sanitize malicious URIs, e.g.:
// '/en/\\example.org → /en/%5C%5Cexample.org'
// '/en////example.org → /en/example.org'
return pathname.replace(/\\/g, '%5C').replace(/\/+/g, '/');
}
export { applyBasePath, formatPathname, formatPathnameTemplate, formatTemplatePathname, getBestMatchingDomain, getHost, getInternalTemplate, getLocaleAsPrefix, getLocalePrefixes, getNormalizedPathname, getPathnameMatch, getRouteParams, isLocaleSupportedOnDomain, sanitizePathname };