next-intl
Version:
Internationalization (i18n) for Next.js
140 lines (132 loc) • 4.89 kB
JavaScript
function isRelativeHref(href) {
const pathname = typeof href === 'object' ? href.pathname : href;
return pathname != null && !pathname.startsWith('/');
}
function isLocalHref(href) {
if (typeof href === 'object') {
return href.host == null && href.hostname == null;
} else {
const hasProtocol = /^[a-z]+:/i.test(href);
return !hasProtocol;
}
}
function isLocalizableHref(href) {
return isLocalHref(href) && !isRelativeHref(href);
}
function unprefixPathname(pathname, prefix) {
return pathname.replace(new RegExp(`^${prefix}`), '') || '/';
}
function prefixPathname(prefix, pathname) {
let localizedHref = prefix;
// Avoid trailing slashes
if (/^\/(\?.*)?$/.test(pathname)) {
pathname = pathname.slice(1);
}
localizedHref += pathname;
return localizedHref;
}
function hasPathnamePrefixed(prefix, pathname) {
return pathname === prefix || pathname.startsWith(`${prefix}/`);
}
function hasTrailingSlash() {
try {
// Provided via `env` setting in `next.config.js` via the plugin
return process.env._next_intl_trailing_slash === 'true';
} catch {
return false;
}
}
function getLocalizedTemplate(pathnameConfig, locale, internalTemplate) {
return typeof pathnameConfig === 'string' ? pathnameConfig : pathnameConfig[locale] || internalTemplate;
}
function normalizeTrailingSlash(pathname) {
const trailingSlash = hasTrailingSlash();
const [path, ...hashParts] = pathname.split('#');
const hash = hashParts.join('#');
let normalizedPath = path;
if (normalizedPath !== '/') {
const pathnameEndsWithSlash = normalizedPath.endsWith('/');
if (trailingSlash && !pathnameEndsWithSlash) {
normalizedPath += '/';
} else if (!trailingSlash && pathnameEndsWithSlash) {
normalizedPath = normalizedPath.slice(0, -1);
}
}
if (hash) {
normalizedPath += '#' + hash;
}
return normalizedPath;
}
function matchesPathname(/** E.g. `/users/[userId]-[userName]` */
template, /** E.g. `/users/23-jane` */
pathname) {
const normalizedTemplate = normalizeTrailingSlash(template);
const normalizedPathname = normalizeTrailingSlash(pathname);
const regex = templateToRegex(normalizedTemplate);
return regex.test(normalizedPathname);
}
function getLocalePrefix(locale, localePrefix) {
return localePrefix.mode !== 'never' && localePrefix.prefixes?.[locale] ||
// We return a prefix even if `mode: 'never'`. It's up to the consumer
// to decide to use it or not.
getLocaleAsPrefix(locale);
}
function getLocaleAsPrefix(locale) {
return '/' + locale;
}
function templateToRegex(template) {
const regexPattern = template
// Replace optional catchall ('[[...slug]]')
.replace(/\[\[(\.\.\.[^\]]+)\]\]/g, '?(.*)')
// Replace catchall ('[...slug]')
.replace(/\[(\.\.\.[^\]]+)\]/g, '(.+)')
// Replace regular parameter ('[slug]')
.replace(/\[([^\]]+)\]/g, '([^/]+)');
return new RegExp(`^${regexPattern}$`);
}
function isOptionalCatchAllSegment(pathname) {
return pathname.includes('[[...');
}
function isCatchAllSegment(pathname) {
return pathname.includes('[...');
}
function isDynamicSegment(pathname) {
return pathname.includes('[');
}
function comparePathnamePairs(a, b) {
const pathA = a.split('/');
const pathB = b.split('/');
const maxLength = Math.max(pathA.length, pathB.length);
for (let i = 0; i < maxLength; i++) {
const segmentA = pathA[i];
const segmentB = pathB[i];
// If one of the paths ends, prioritize the shorter path
if (!segmentA && segmentB) return -1;
if (segmentA && !segmentB) return 1;
if (!segmentA && !segmentB) continue;
// Prioritize static segments over dynamic segments
if (!isDynamicSegment(segmentA) && isDynamicSegment(segmentB)) return -1;
if (isDynamicSegment(segmentA) && !isDynamicSegment(segmentB)) return 1;
// Prioritize non-catch-all segments over catch-all segments
if (!isCatchAllSegment(segmentA) && isCatchAllSegment(segmentB)) return -1;
if (isCatchAllSegment(segmentA) && !isCatchAllSegment(segmentB)) return 1;
// Prioritize non-optional catch-all segments over optional catch-all segments
if (!isOptionalCatchAllSegment(segmentA) && isOptionalCatchAllSegment(segmentB)) {
return -1;
}
if (isOptionalCatchAllSegment(segmentA) && !isOptionalCatchAllSegment(segmentB)) {
return 1;
}
if (segmentA === segmentB) continue;
}
// Both pathnames are completely static
return 0;
}
function getSortedPathnames(pathnames) {
return pathnames.sort(comparePathnamePairs);
}
function isPromise(value) {
// https://github.com/amannn/next-intl/issues/1711
return typeof value.then === 'function';
}
export { getLocaleAsPrefix, getLocalePrefix, getLocalizedTemplate, getSortedPathnames, hasPathnamePrefixed, isLocalizableHref, isPromise, matchesPathname, normalizeTrailingSlash, prefixPathname, templateToRegex, unprefixPathname };