UNPKG

next-intl

Version:

Internationalization (i18n) for Next.js

140 lines (132 loc) 4.89 kB
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 };