UNPKG

@mcpronovost/okp-router

Version:

A lightweight routing solution specifically designed for Vite-based projects with multilingual support.

198 lines (179 loc) 6.22 kB
import type { RouteType, RouteModulesType } from "./types"; import { routerConfig, REGEX } from "./config"; import { getLangAndUri } from "./utils"; /** * Get all routes * @param modules Route modules from Vite's glob import * @returns Object with all routes * @since 0.1.1 */ export const getRoutes = ( modules?: RouteModulesType | undefined ): Record<string, RouteType> => { if (modules || routerConfig.routeModules) { return (() => { return Object.values(modules || routerConfig.routeModules || {}).reduce< Record<string, RouteType> >((acc, module) => { const firstRoute = Object.values(module)[0] as unknown as Record< string, RouteType >; return { ...acc, ...firstRoute }; }, {}); })(); } return routerConfig.routes; }; /** * Recursively finds a route by matching the URI to translations in the route map * @param uri The URI path to match against route translations * @param lang The language code to use for matching (e.g., "en" or "fr") * @param routesList Optional route map to search through. Defaults to global routes if not provided * @param parentPath Optional dot-notation path of parent routes. Used internally for recursion * @returns A tuple containing [fullRoutePath, routeObject] if found, undefined otherwise * @since 0.1.0 */ export const getRoute = ( uri: string, lang: string = routerConfig.currentLang || routerConfig.defaultLang, routesList?: Record<string, RouteType>, parentPath: string = "" ): [string, RouteType] => { if (!routesList) routesList = getRoutes(); if (uri === "/") uri = ""; const params = {}; for (const [key, route] of Object.entries(routesList)) { const fullPath = parentPath ? `${parentPath}.${key}` : key; const routePath = route.paths[lang]; // Handle dynamic path segments if (routePath.toString().includes("{")) { // Extract parameter names from the route path const paramNames = [...routePath.matchAll(REGEX.PARAM)].map( (match) => match[1] ); const pathPattern = routePath.replace(REGEX.PARAM, "([^/]+)"); const regex = new RegExp(`^${pathPattern}$`); const matches = uri.match(regex); if (matches) { // Store captured values with their parameter names paramNames.forEach((name, index) => { params[name] = matches[index + 1]; }); return [fullPath, { ...route, params }]; } } else if (routePath === uri) { return [fullPath, { ...route, params }]; } // Check for child routes recursively if (route.children) { const childUri = `${route.paths[lang]}/`; if ( uri.startsWith(childUri) || (routePath.includes("{") && new RegExp( `^${routePath.replace(REGEX.PARAM_REPLACE, "[^/]+")}/` ).test(uri)) ) { const nextParentPath = parentPath ? `${parentPath}.${key}` : key; // Extract params from current level if it's a dynamic route if (routePath.includes("{")) { const paramNames = [...routePath.matchAll(/{([^}]+)}/g)].map( (match) => match[1] ); const pathPattern = routePath.replace(REGEX.PARAM_REPLACE, "([^/]+)"); const matches = uri.match(new RegExp(`^${pathPattern}/`)); if (matches) { paramNames.forEach((name, index) => { params[name] = matches[index + 1]; }); } } const childRoute = getRoute( uri.replace( new RegExp(`^${routePath.replace(/{[^}]+}/g, "[^/]+")}/`), "" ), lang, route.children, nextParentPath ); if (childRoute) { // Merge params from child route with current params return [childRoute[0], childRoute[1]]; } } } } return [ uri, { view: "errors/404", paths: {}, auth: false, props: {}, params: {}, }, ]; }; /** * Find the localized route path from a specific language view name * @param uri The target view name * @param toLang The target language * @param fromLang The specific language to translate view name from * @param additionalParams Optional additional parameters to pass to the new route * @returns The localized route path in the target language, or the original view name if no translation is found * @since 0.1.0 */ export const getLocalizedRoute = ( uri: string, toLang: string = routerConfig.currentLang || routerConfig.defaultLang, fromLang: string = "en", additionalParams?: Record<string, string> ): string => { // Find the current route based on the URI and current language const currentRoute = getRoute(uri, fromLang); if (!currentRoute) return `/${toLang}/${uri}`; const [routePath, routeData] = currentRoute; const params = { ...routeData.params, ...additionalParams }; // Split the route path to handle nested routes const routeParts = routePath.split("."); let routesList = getRoutes(); let toPath = ""; // Build the new path by traversing the route tree for (let i = 0; i < routeParts.length; i++) { const part = routeParts[i]; const currentPart = routesList[part]; if (currentPart) { toPath += (i > 0 ? "/" : "") + currentPart.paths[toLang]; routesList = currentPart.children || {}; } } // Replace params in the path if (params) { for (const [key, value] of Object.entries(params)) { toPath = toPath.replace(`{${key}}`, value); } } return `/${toLang}/${toPath}`; }; /** * Switch the language of the current route * @param toLang The target language * @param additionalParams Optional additional parameters to pass to the new route * @returns The new route path in the target language * @since 0.4.1 */ export const switchRouteLanguage = ( toLang: string = routerConfig.currentLang || routerConfig.defaultLang || "en", additionalParams?: Record<string, string> ): string => { const { langCode, uri } = getLangAndUri(window.location.pathname); const newRoute = getLocalizedRoute( uri, toLang, langCode, additionalParams ); return newRoute; };