@i18n-micro/route-strategy 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 s00d
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,228 @@
1
+ # @i18n-micro/route-strategy
2
+
3
+ High‑performance route generation and localization strategies for **Nuxt I18n Micro**.
4
+
5
+ This package is responsible for turning Nuxt page definitions (`NuxtPage[]`) into a fully localized route tree for all supported strategies:
6
+
7
+ - `no_prefix`
8
+ - `prefix`
9
+ - `prefix_except_default`
10
+ - `prefix_and_default`
11
+
12
+ It is used by the main Nuxt module at build/extend‑pages time and is designed to be:
13
+
14
+ - deterministic (stable snapshots),
15
+ - fast (single pass over pages, no heavy allocations),
16
+ - and testable (Jest snapshot suite under `tests/`).
17
+
18
+ ---
19
+
20
+ ## Overview
21
+
22
+ At a high level, `@i18n-micro/route-strategy`:
23
+
24
+ - takes the raw Nuxt `pages` array;
25
+ - applies the selected i18n strategy;
26
+ - expands each page into zero, one or many localized routes;
27
+ - wires in:
28
+ - `globalLocaleRoutes` (custom paths per locale),
29
+ - `filesLocaleRoutes` (file‑level locale paths),
30
+ - `routeLocales` (locale restrictions),
31
+ - `noPrefixRedirect` and `includeDefaultLocaleRoute` flags;
32
+ - keeps aliases, children and internal/excluded routes consistent.
33
+
34
+ All of this is done without any Nuxt runtime / Vue Router dependency – it operates purely on plain `NuxtPage` objects, so it is safe to run in Node during build and easy to test.
35
+
36
+ ---
37
+
38
+ ## Core Concepts
39
+
40
+ ### Strategies
41
+
42
+ The generator supports the same strategies as the runtime:
43
+
44
+ - **`no_prefix`**
45
+ - URLs have **no locale prefix** (`/about`, `/kontakt`).
46
+ - Locale is handled via cookies / runtime logic, not via path.
47
+ - `globalLocaleRoutes` are used to generate per‑locale variants where appropriate, but the *visible* URLs stay prefix‑less.
48
+
49
+ - **`prefix`**
50
+ - All localized routes are prefixed: `/en/about`, `/de/ueber-uns`, etc.
51
+ - There is **no unprefixed** default route; every locale uses its own prefix.
52
+
53
+ - **`prefix_except_default`**
54
+ - Default locale uses no prefix (`/about`).
55
+ - Non‑default locales are prefixed (`/de/ueber-uns`).
56
+ - This is the most complex strategy around children, aliases and custom paths.
57
+
58
+ - **`prefix_and_default`**
59
+ - Default locale is available **both** as unprefixed and prefixed:
60
+ - `/about` and `/en/about` can coexist.
61
+ - Non‑default locales behave like in `prefix`.
62
+
63
+ ### Inputs and configuration
64
+
65
+ The main entry point is the `RouteGenerator` class:
66
+
67
+ ```ts
68
+ import type { NuxtPage } from '@nuxt/schema'
69
+ import { RouteGenerator } from '@i18n-micro/route-strategy'
70
+
71
+ const generator = new RouteGenerator({
72
+ locales, // Array<{ code, iso, name, baseUrl?, baseDefault? }>
73
+ defaultLocaleCode, // e.g. 'en'
74
+ strategy, // 'no_prefix' | 'prefix' | 'prefix_except_default' | 'prefix_and_default'
75
+ globalLocaleRoutes, // Optional: per‑path custom routes per locale
76
+ filesLocaleRoutes, // Optional: per‑file routes extracted at build time
77
+ routeLocales, // Optional: per‑route locale restrictions
78
+ noPrefixRedirect, // Optional: behavior for redirect helpers in no_prefix
79
+ })
80
+
81
+ const pages: NuxtPage[] = [
82
+ { path: '/about', name: 'about' },
83
+ // ...
84
+ ]
85
+
86
+ generator.extendPages(pages)
87
+ // `pages` is now mutated in‑place and contains localized routes
88
+ ```
89
+
90
+ Key config fields:
91
+
92
+ - **`globalLocaleRoutes`**:
93
+ - Map from *canonical path* or *route name* to per‑locale paths:
94
+ - Example:
95
+ ```ts
96
+ const globalLocaleRoutes = {
97
+ '/about': {
98
+ en: '/about',
99
+ de: '/ueber-uns',
100
+ ru: '/o-nas',
101
+ },
102
+ }
103
+ ```
104
+
105
+ - **`filesLocaleRoutes`**:
106
+ - Map extracted from files (e.g. Vite/nuxt loader) that describes custom paths defined in code (`$defineI18nRoute`‑like APIs).
107
+ - Used as a fallback when there is no explicit `globalLocaleRoutes` entry.
108
+
109
+ - **`routeLocales`**:
110
+ - Restricts which locales are allowed for a given page path:
111
+ ```ts
112
+ const routeLocales = {
113
+ '/about': ['en', 'de'], // 'ru' will not get localized variants
114
+ }
115
+ ```
116
+
117
+ ---
118
+
119
+ ## Internal Architecture (for Contributors)
120
+
121
+ > This section is intended for developers working on `@i18n-micro/route-strategy` itself.
122
+
123
+ The package is structured around **strategies** and a small **core**:
124
+
125
+ - `src/route-generator.ts`
126
+ - Thin facade around strategy selection and Nuxt `extendPages` hook contract.
127
+ - `src/core/context.ts`
128
+ - `GeneratorContext`:
129
+ - holds locales, default locale, strategy;
130
+ - provides helpers to query:
131
+ - `getAllowedLocales` (applies `routeLocales`);
132
+ - `getCustomPath` (combines `globalLocaleRoutes` + `filesLocaleRoutes`);
133
+ - `localizedPaths` map (used to resolve children/aliases).
134
+ - `src/core/localized-paths.ts`
135
+ - Responsible for building a normalized map of `{ pathKey -> perLocalePath }`.
136
+ - Used heavily by strategies to resolve nested/child routes and aliases.
137
+ - `src/core/alias.ts`
138
+ - `generateAliasRoutes`:
139
+ - creates proper alias routes for each localized variant;
140
+ - ensures that aliases inherit children so that `/company/team` works when `/about/team` exists.
141
+ - `src/strategies/abstract.ts`
142
+ - `BaseStrategy` and helpers:
143
+ - common logic for recursion over children;
144
+ - helpers for looking up custom paths;
145
+ - ensures immutability and deterministic ordering.
146
+ - `src/strategies/*.ts`
147
+ - Concrete strategies:
148
+ - `no-prefix.ts`
149
+ - `prefix.ts`
150
+ - `prefix-except-default.ts`
151
+ - `prefix-and-default.ts`
152
+ - Each strategy implements:
153
+ - `processPage(page, context)` → `NuxtPage[]`:
154
+ - decides how the original page should be localized (or left as‑is);
155
+ - uses `localizeChildren` / `localizeChildrenAllLocales` from `BaseStrategy` for deep trees;
156
+ - respects `internal`/excluded routes.
157
+
158
+ The implementation is heavily covered by snapshot tests under `tests/`:
159
+
160
+ - `basic.test.ts` – common/simple scenarios.
161
+ - `paths-and-alias.test.ts` – aliases, internal paths, Cloudflare Pages, `filesLocaleRoutes`.
162
+ - `locale-restrictions.test.ts` – `routeLocales` behavior.
163
+ - `strategies.test.ts` – cross‑strategy matrix.
164
+ - `deep-nesting.test.ts` / `advanced.test.ts` – complex trees.
165
+ - `critical-scenarios.test.ts` – regression cases.
166
+
167
+ When making changes, always run:
168
+
169
+ ```bash
170
+ pnpm --filter @i18n-micro/route-strategy test
171
+ ```
172
+
173
+ Snapshots are considered part of the public contract for this package – avoid changing them unless you are intentionally changing behavior and understand the migration impact.
174
+
175
+ ---
176
+
177
+ ## Developer Guidelines
178
+
179
+ - **Do not depend on Nuxt/Vue Router here**:
180
+ - This package must stay framework‑agnostic and operate on `NuxtPage`‑like POJOs only.
181
+ - All runtime behaviour (redirects, `$localeRoute`, `$localePath`, etc.) is handled by other packages (`@i18n-micro/core`, `@i18n-micro/path-strategy`, runtime plugins).
182
+
183
+ - **Keep strategies deterministic**:
184
+ - Given the same input pages and config, output must be stable.
185
+ - Do not introduce randomization or time‑dependent behavior.
186
+
187
+ - **Prefer small, composable helpers**:
188
+ - Logic that is shared between multiple strategies should live in:
189
+ - `BaseStrategy`,
190
+ - or dedicated helpers in `core/`.
191
+ - Avoid copying complex conditionals into each strategy.
192
+
193
+ - **Respect performance**:
194
+ - `extendPages` is called at build time, and can run on large apps.
195
+ - Avoid unnecessary deep clones; reuse objects where safe.
196
+ - Prefer simple loops over heavy abstractions.
197
+
198
+ ---
199
+
200
+ ## Testing & Contribution
201
+
202
+ - Tests live in `packages/route-strategy/tests`.
203
+ - Jest config: `packages/route-strategy/jest.config.cjs`.
204
+ - To run only these tests:
205
+
206
+ ```bash
207
+ pnpm --filter @i18n-micro/route-strategy test
208
+ ```
209
+
210
+ If you contribute new behavior:
211
+
212
+ 1. Add or update a dedicated test file.
213
+ 2. Prefer asserting on the full `pages` array via snapshots to catch regressions.
214
+ 3. Document non‑obvious behavior in this README or in inline comments near the relevant strategy/core function.
215
+
216
+ ---
217
+
218
+ ## Relationship to Other Packages
219
+
220
+ - **`@i18n-micro/core`**:
221
+ - Uses the routes generated here to implement runtime helpers (`$t`, `$localeRoute`, `$switchLocaleRoute`, etc.).
222
+ - Assumes the naming and path conventions from `@i18n-micro/route-strategy` are stable.
223
+
224
+ - **`@i18n-micro/path-strategy`**:
225
+ - Implements strategy‑aware **frontend** path building for `$localeRoute` / `$localePath`.
226
+ - Must remain consistent with how `@i18n-micro/route-strategy` names/structures routes.
227
+
228
+ If you change route naming or generation rules here, you almost certainly need to review these packages as well.
@@ -0,0 +1,6 @@
1
+ import { NuxtPage } from '@nuxt/schema';
2
+ /**
3
+ * Generates dedicated routes for page aliases.
4
+ * Mirrors the legacy handleAliasRoutes behavior in the original RouteGenerator.
5
+ */
6
+ export declare function generateAliasRoutes(page: NuxtPage, localeCodes: string[], customRegex?: string | RegExp, localizedRouteNamePrefix?: string): NuxtPage[];
@@ -0,0 +1,20 @@
1
+ import { NuxtPage } from '@nuxt/schema';
2
+ /**
3
+ * Joins parent path and child segment.
4
+ * If childSegment is absolute (starts with `/`) — returns it (normalized).
5
+ * Otherwise, performs join(parentPath, childSegment) with normalization.
6
+ */
7
+ export declare function resolveChildPath(parentPath: string, childSegment: string): string;
8
+ export interface CreateRouteOptions {
9
+ path: string;
10
+ name?: string;
11
+ children?: NuxtPage[];
12
+ meta?: Record<string, unknown>;
13
+ /** When undefined, alias is cleared (for the main localized route when alias routes are created separately). */
14
+ alias?: string[] | undefined;
15
+ }
16
+ /**
17
+ * Creates a new NuxtPage object from the original page and provided options.
18
+ * Copies meta, component, file, etc.; provided options override the respective fields.
19
+ */
20
+ export declare function createRoute(page: NuxtPage, options: CreateRouteOptions): NuxtPage;
@@ -0,0 +1,53 @@
1
+ import { NuxtPage } from '@nuxt/schema';
2
+ import { GlobalLocaleRoutes, Locale, Strategies } from '@i18n-micro/types';
3
+ import { LocaleRoutesConfig } from '../strategies/types';
4
+ import { LocalizedPathsMap } from './localized-paths';
5
+ export interface GeneratorContextOptions {
6
+ locales: Locale[];
7
+ defaultLocaleCode: string;
8
+ strategy: Strategies;
9
+ globalLocaleRoutes: GlobalLocaleRoutes;
10
+ filesLocaleRoutes: LocaleRoutesConfig;
11
+ routeLocales: Record<string, string[]>;
12
+ pages: NuxtPage[];
13
+ excludePatterns?: (string | RegExp)[];
14
+ customRegex?: string | RegExp;
15
+ noPrefixRedirect?: boolean;
16
+ localizedRouteNamePrefix?: string;
17
+ /** Resolved path to the locale-redirect component for prefix strategy fallback route. */
18
+ fallbackRedirectComponentPath?: string;
19
+ /** Raw globalLocaleRoutes for fallback route meta (locale-redirect component). */
20
+ rawGlobalLocaleRoutes?: GlobalLocaleRoutes;
21
+ }
22
+ export declare class GeneratorContext {
23
+ readonly locales: Locale[];
24
+ readonly defaultLocale: Locale;
25
+ readonly strategy: Strategies;
26
+ readonly globalLocaleRoutes: LocaleRoutesConfig;
27
+ readonly filesLocaleRoutes: LocaleRoutesConfig;
28
+ readonly routeLocales: Record<string, string[]>;
29
+ readonly localizedPaths: LocalizedPathsMap;
30
+ readonly activeLocaleCodes: string[];
31
+ readonly excludePatterns: (string | RegExp)[] | undefined;
32
+ readonly customRegex: string | RegExp | undefined;
33
+ readonly noPrefixRedirect: boolean;
34
+ readonly localizedRouteNamePrefix: string;
35
+ readonly fallbackRedirectComponentPath: string | undefined;
36
+ readonly rawGlobalLocaleRoutes: GlobalLocaleRoutes | undefined;
37
+ constructor(options: GeneratorContextOptions);
38
+ private findLocaleByCode;
39
+ /**
40
+ * Returns the list of locale codes allowed for a page (respecting routeLocales).
41
+ */
42
+ getAllowedLocales(pagePath: string, pageName: string): string[];
43
+ /**
44
+ * Returns a custom path for a given locale by original page path, if any.
45
+ */
46
+ getCustomPath(originalPath: string, localeCode: string): string | undefined;
47
+ /**
48
+ * Returns a map of custom paths (locale → path) for a page by path key or name.
49
+ */
50
+ getCustomPathsForPage(originalPath: string, pageName: string): Record<string, string> | undefined;
51
+ hasLocaleRestrictions(pagePath: string, pageName: string): boolean;
52
+ filterLocaleCodesWithoutCustomPaths(fullPath: string): string[];
53
+ }
@@ -0,0 +1,20 @@
1
+ import { NuxtPage } from '@nuxt/schema';
2
+ import { LocaleRoutesConfig } from '../strategies/types';
3
+ export type LocalizedPathsMap = Record<string, Record<string, string>>;
4
+ /**
5
+ * Single key format for localizedPaths: without a leading slash so that it
6
+ * matches:
7
+ * - keys from the Nuxt module (routePath from page files),
8
+ * - and lookups from strategies (resolveChildPath results).
9
+ */
10
+ export declare function pathKeyForLocalizedPaths(fullPath: string): string;
11
+ /**
12
+ * Extracts a map of custom paths (path key → locale → path) from:
13
+ * - pages,
14
+ * - globalLocaleRoutes,
15
+ * - filesLocaleRoutes.
16
+ *
17
+ * Walks children recursively. Keys are stored without a leading slash for
18
+ * compatibility with the Nuxt module and strategies.
19
+ */
20
+ export declare function extractLocalizedPaths(pages: NuxtPage[], globalLocaleRoutes: LocaleRoutesConfig, filesLocaleRoutes: LocaleRoutesConfig, parentPath?: string): LocalizedPathsMap;
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const w=require("node:path"),S=require("node:fs"),k=require("@i18n-micro/core");function $(l){return l.split("/").map(e=>e.startsWith("[...")&&e.endsWith("]")?`:${e.substring(4,e.length-1)}(.*)*`:e.startsWith("[")&&e.endsWith("]")?`:${e.substring(1,e.length-1)}`:e).join("/")}const R=l=>{if(!l)return"";const e=w.posix.normalize(l).replace(/\/+$/,"");return e==="."?"":e},g=l=>l.startsWith("/")?l.slice(1):l;function O(...l){return w.posix.join(...l)}function H(l){if(!(typeof l>"u"))return l.startsWith("/")&&l.endsWith("/")?l?.slice(1,-1):l}function K(l){return!l||!/[\u0080-\uFFFF]/.test(l)?l:l.split("/").map(e=>!e||e.startsWith(":")||!/[\u0080-\uFFFF]/.test(e)?e:encodeURI(e)).join("/")}function N(l,e,t){const a=H(t?.toString()),o=a||(Array.isArray(l)?l.join("|"):l),s=K(e);return R(w.posix.join("/",`:locale(${o})`,s))}function M(l){const e=K(l);return R(e)}const b=l=>l.map(e=>({...e})),T=l=>!!(l.redirect&&!l.file),F=(l,e,t,a="localized-")=>t?`${a}${l}-${e}`:`${a}${l}`,C=(l,e)=>l??(e??"").replace(/[^a-z0-9]/gi,"-").replace(/^-+|-+$/g,""),V=(l,e,t,a)=>t&&!(l===e.code&&!a),I=(l,e,t)=>(typeof l=="string"?l:l.code)===e.code&&!t,J=[/^\/sitemap.*\.xml$/,/^\/sitemap\.xml$/,/^\/robots\.txt$/,/^\/favicon\.ico$/,/^\/apple-touch-icon.*\.png$/,/^\/manifest\.json$/,/^\/sw\.js$/,/^\/workbox-.*\.js$/,/\.(xml|txt|ico|json|js|css|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)$/];function E(l,e){if(/(?:^|\/)__[^/]+/.test(l))return!0;for(const t of J)if(t.test(l))return!0;if(e){for(const t of e)if(typeof t=="string"){if(t.includes("*")||t.includes("?")){if(new RegExp(t.replace(/\*/g,".*").replace(/\?/g,".")).test(l))return!0}else if(l===t||l.startsWith(t))return!0}else if(t instanceof RegExp&&t.test(l))return!0}return!1}function Q(l,e){const t=l.reduce((o,s)=>{const i=o.find(r=>r.code===s.code);return i?Object.assign(i,s):o.push(s),o},[]).filter(o=>!o.disabled),a=t.find(o=>o.code===e)??{code:e};return{locales:t,defaultLocale:a}}const X=(l,e)=>l??(e??"").replace(/[^a-z0-9]/gi,"-").replace(/^-+|-+$/g,"");function j(l){return g(l)||"/"}function _(l,e,t,a=""){const o={};return l.forEach(s=>{const i=X(s.name,s.path),r=R(O(a,s.path??"")),n=j(r),d=$(r),c=e[n]||e[d]||e[i];if(c)typeof c=="object"&&(o[n]=c);else{const h=t[i];h&&typeof h=="object"&&!Array.isArray(h)&&(o[n]=h)}if(s.children?.length){const h=R(O(a,s.path??""));Object.assign(o,_(s.children,e,t,h))}}),o}function Y(l){const e={};if(!l)return e;for(const t in l){const a=$(t),o=l[t];if(typeof o=="object"){const s={};for(const i in o){const r=o[i];r&&(s[i]=$(r))}e[a]=s}else e[a]=o}return e}class Z{constructor(e){this.locales=e.locales,this.defaultLocale=this.findLocaleByCode(e.locales,e.defaultLocaleCode)??{code:e.defaultLocaleCode},this.strategy=e.strategy,this.globalLocaleRoutes=Y(e.globalLocaleRoutes??{}),this.filesLocaleRoutes=e.filesLocaleRoutes??{},this.routeLocales=e.routeLocales??{},this.localizedPaths=_(e.pages,this.globalLocaleRoutes,this.filesLocaleRoutes),this.activeLocaleCodes=this.locales.filter(t=>t.code!==this.defaultLocale.code||k.isPrefixAndDefaultStrategy(this.strategy)||k.isPrefixStrategy(this.strategy)).map(t=>t.code),this.excludePatterns=e.excludePatterns,this.customRegex=e.customRegex,this.noPrefixRedirect=e.noPrefixRedirect??!1,this.localizedRouteNamePrefix=e.localizedRouteNamePrefix??"localized-",this.fallbackRedirectComponentPath=e.fallbackRedirectComponentPath,this.rawGlobalLocaleRoutes=e.rawGlobalLocaleRoutes}findLocaleByCode(e,t){return e.find(a=>a.code===t)}getAllowedLocales(e,t){const a=this.routeLocales[e]??this.routeLocales[t];return a?.length?a.filter(o=>this.locales.some(s=>s.code===o)):this.locales.map(o=>o.code)}getCustomPath(e,t){const a=j(e);return this.localizedPaths[a]?.[t]}getCustomPathsForPage(e,t){const a=j(e);return this.localizedPaths[a]??this.localizedPaths[t]}hasLocaleRestrictions(e,t){return!!(this.routeLocales[e]??this.routeLocales[t])}filterLocaleCodesWithoutCustomPaths(e){const t=j(e);return this.activeLocaleCodes.filter(a=>!this.localizedPaths[t]?.[a])}}function z(l,e){const t=(e??"").trim();return t.startsWith("/")?R(t):R(w.posix.join(l||"",t))}function p(l,e){const{path:t,name:a,children:o,meta:s,alias:i}=e,r={...l,path:t};return a!==void 0&&(r.name=a),o!==void 0&&(r.children=o),s!==void 0&&(r.meta={...l.meta,...s}),i!==void 0?(r.alias=i,r.meta={...r.meta,alias:i}):"alias"in e&&(r.alias=void 0,r.meta={...r.meta,alias:void 0}),r}function ee(l){const e=l.alias,t=l.meta?.alias,a=e??t;return Array.isArray(a)?a:[]}function D(l,e,t,a="localized-"){const o=ee(l);if(!o.length)return[];const s=[];for(const i of o){const r=N(e,i,t);s.push(p(l,{path:r,name:`${a}${l.name??""}`,alias:void 0,meta:{alias:void 0},children:l.children}))}return s}class W{processPage(e,t){if(e.path===void 0)throw new Error("page.path is required");if(T(e))return[e];if(e.path&&E(e.path,t.excludePatterns))return[e];const a=e.path??"",o=C(e.name,e.path),s=$(a);return t.globalLocaleRoutes[o]===!1||t.globalLocaleRoutes[s]===!1?[e]:this.generateVariants(e,t)}postProcess(e,t){return e}localizeChildren(e,t,a,o,s,i){return e.flatMap(r=>this.localizeChild(r,t,a,o,s,i))}localizeChildrenAllLocales(e,t,a,o,s){return e.flatMap(i=>this.localizeChildAllLocales(i,t,a,o,s))}localizeChildAllLocales(e,t,a,o,s){const i=z(a,e.path??""),r=z(t,e.path??""),n=j(r),d=s.localizedPaths[n];if(d)return o.flatMap(f=>{const P=d[f]??z(t,e.path??"");return this.localizeChild(e,P,a,f,s,!0)});const c=R(e.path??""),h=g(c),u=this.localizeChildrenAllLocales(b(e.children??[]),r,i,o,s),m=`${s.localizedRouteNamePrefix}${e.name??""}`;return[p(e,{path:h,name:m,children:u})]}localizeChild(e,t,a,o,s,i){const r=z(a,e.path??""),n=s.getCustomPath(r,o),d=g(R(n||(e.path??""))),c=n??z(t,e.path??""),h=this.localizeChildren(b(e.children??[]),c,r,o,s,i),u=C(e.name,e.path),m=`${s.localizedRouteNamePrefix}${u}-${o}`;return[p(e,{path:d,name:m,children:h})]}buildRoutePathForLocales(e,t,a,o,s,i,r){if(o){const n=e.includes(r);return i||!n?N(e,a,s):R(a)}return N(e,t,s)}}class te extends W{generateVariants(e,t){const a=e.path??"",o=C(e.name,e.path),s=t.getAllowedLocales(a,o),i=t.getCustomPathsForPage(a,o),r=b(e.children??[]),n=[];if(n.push(e),i)for(const d of s){const c=i[d];if(!c)continue;const h=R(c),u=R(a);if(h===u)continue;const m=M(c),f=F(o,d,!0),P=this.localizeChildrenForNoPrefix(r,h,a,a,d,t,1);n.push(p(e,{path:m,name:f,children:P,alias:[],meta:{alias:[]}})),t.noPrefixRedirect&&d===t.defaultLocale.code&&h!==u&&(e.redirect=h)}return n.push(...D(e,s,t.customRegex,t.localizedRouteNamePrefix)),n}localizeChildrenForNoPrefix(e,t,a,o,s,i,r){return e.map(n=>{const d=z(a,n.path??"");let c,h;if(r===1){const P=i.getCustomPath(d,s);if(P){const L=R(P);c=g(L),h=L}else{const L=R(n.path??"");c=g(L),h=z(t,n.path??"")}}else{const P=R(n.path??"");c=g(P),h=z(t,n.path??"")}const u=this.localizeChildrenForNoPrefix(b(n.children??[]),h,d,o,s,i,r+1),m=C(n.name,n.path),f=F(m,s,!0);return p(n,{path:c,name:f,children:u})})}}function ae(l){const e=l.alias??l.meta?.alias;return Array.isArray(e)?e:[]}class se extends W{generateVariants(e,t){const a=e.path??"",o=C(e.name,e.path),s=t.getAllowedLocales(a,o),i=t.getCustomPathsForPage(a,o),r=b(e.children??[]),n=[];if(i)for(const c of s){const h=i[c],u=h?R(h):a,m=N(c,u,t.customRegex),f=F(o,c,!!h,t.localizedRouteNamePrefix),P=h??a,L=this.localizeChildren(r,P,a,c,t,!0);n.push(p(e,{path:m,name:f,children:L,alias:[],meta:{alias:[]}}))}else{const c=N(s,a,t.customRegex),h=F(o,s[0],!1,t.localizedRouteNamePrefix),u=this.localizeChildrenAllLocales(r,a,a,s,t);n.push(p(e,{path:c,name:h,children:u,alias:[],meta:{alias:[]}}))}const d=ae(e);if(d.length)for(const c of d){const h=N(s,c,t.customRegex);n.push(p(e,{path:h,name:`${t.localizedRouteNamePrefix}${e.name??""}`,alias:void 0,meta:{alias:void 0}}))}return n}postProcess(e,t){const a=e.filter(o=>{if(o.name==="index"&&o.path==="/")return!1;const s=o.path??"";return!!(s&&E(s,t.excludePatterns)||t.globalLocaleRoutes[o.name??""]===!1||/^\/:locale/.test(s)||s==="/")});return t.fallbackRedirectComponentPath&&a.push({path:"/:pathMatch(.*)*",name:"custom-fallback-route",file:t.fallbackRedirectComponentPath,meta:{globalLocaleRoutes:t.rawGlobalLocaleRoutes??t.globalLocaleRoutes}}),a}}class oe extends W{generateVariants(e,t){const a=e.path??"",o=C(e.name,e.path),s=t.getAllowedLocales(a,o),i=t.getCustomPathsForPage(a,o),r=b(e.children??[]),n=[],d=t.defaultLocale.code;n.push(e);const c=t.locales.filter(u=>s.includes(u.code));if(c.length>0)if(i)for(const u of c){const m=i[u.code];if(m)if(u.code===d){const f=this.createLocalizedRoute(e,[u.code],r,!0,m,t.customRegex,!1,u.code,a,t);f&&n.push(f);const P=this.createLocalizedRoute(e,[u.code],r,!0,m,t.customRegex,!0,u.code,a,t);P&&n.push(P)}else{const f=this.createLocalizedRoute(e,[u.code],r,!0,m,t.customRegex,!1,u.code,a,t);f&&n.push(f)}else{const f=this.createLocalizedRoute(e,[u.code],r,!1,"",t.customRegex,!1,u.code,a,t);f&&n.push(f)}}else{const u=c.map(f=>f.code),m=this.createLocalizedRoute(e,u,r,!1,"",t.customRegex,!1,!0,a,t);m&&n.push(m)}const h=D(e,s,t.customRegex,t.localizedRouteNamePrefix);return h.length&&n.push(...h),n}postProcess(e,t){const a=[],o=[];for(const s of e){const i=s.name??"";typeof i=="string"&&i.startsWith(t.localizedRouteNamePrefix)?o.push(s):a.push(s)}return[...a,...o]}createLocalizedRoute(e,t,a,o,s="",i,r=!1,n=!1,d,c){const h=this.buildRoutePathForLocales(t,e.path??"",encodeURI(s),o,i,r,c.defaultLocale.code);if(!h||!(o&&s)&&h===e.path||t.length===0)return null;const m=t[0];if(!m)return null;const f=d??e.path??"",P=F(C(e.name??"",f),m,o,c.localizedRouteNamePrefix),L=r||m!==c.defaultLocale.code,y=t.length===1?this.localizeChildren(a,h,f,m,c,L):this.localizeChildrenAllLocales(a,h,f,t,c);return p(e,{path:h,name:P,children:y,alias:[],meta:{alias:[]}})}}class le extends W{generateVariants(e,t){const a=e.path??"",o=C(e.name,e.path),s=t.getAllowedLocales(a,o),i=t.getCustomPathsForPage(a,o),r=b(e.children??[]),n=[],d=t.defaultLocale.code;if(s.includes(d)){const u=i?.[d],m=u?R(u):a,f=u?{[d]:u}:{},P=this.createLocalizedChildren(r,a,[d],!1,!1,!1,f,t);n.push(p(e,{path:m,name:e.name,children:P}))}const c=t.locales.filter(u=>s.includes(u.code)&&u.code!==d);if(c.length)if(i)for(const u of c){const m=i[u.code];if(m){const f=this.createLocalizedRoute(e,[u.code],r,!0,m,t.customRegex,!1,u.code,a,t);f&&n.push(f)}else{const f=this.createLocalizedRoute(e,[u.code],r,!1,"",t.customRegex,!1,u.code,a,t);f&&n.push(f)}}else{const u=c.map(f=>f.code),m=this.createLocalizedRoute(e,u,r,!1,"",t.customRegex,!1,!0,a,t);m&&n.push(m)}const h=D(e,s,t.customRegex,t.localizedRouteNamePrefix);return h.length&&n.push(...h),n}postProcess(e,t){const a=[],o=[];for(const i of e){const r=i.name??"";typeof r=="string"&&r.startsWith(t.localizedRouteNamePrefix)?o.push(i):a.push(i)}const s=[...a,...o];return t.fallbackRedirectComponentPath&&s.push({path:"/:pathMatch(.*)*",name:"custom-fallback-route",file:t.fallbackRedirectComponentPath,meta:{globalLocaleRoutes:t.rawGlobalLocaleRoutes??t.globalLocaleRoutes}}),s}createLocalizedChildren(e,t,a,o=!0,s=!1,i=!1,r={},n){return e.flatMap(d=>this.createLocalizedVariants(d,t,a,o,s,i,r,n))}createLocalizedVariants(e,t,a,o,s,i=!1,r,n){const d=R(e.path??""),c=z(t,e.path??""),h=j(c),u=n.localizedPaths[h],m=!!u,f=[];if(!m){const P=g(d),L=this.createLocalizedChildren(b(e.children??[]),z(t,e.path??""),a,o,s,i,r,n),y=this.buildChildRouteName(e.name,i,n);return f.push(p(e,{path:P,name:y,children:L})),f}for(const P of a){const L=r?.[P],y=!!L,x=u?.[P];let A=R(x||(e.path??""));y&&L&&(x?A=R(x):A=z(L,e.path??""));const v=V(P,n.defaultLocale,s,!1)?N(P,A,n.customRegex):A,q=g(v),G=x?R(x):y?L:z(t,e.path??""),B=this.createLocalizedChildren(b(e.children??[]),G,[P],o,s,P,{...r,[P]:G},n),U=this.buildLocalizedRouteName(C(e.name,e.path),P,o,!!u,n);f.push(p(e,{path:q,name:U,children:B}))}return f}buildChildRouteName(e,t,a){return t===!0?`${a.localizedRouteNamePrefix}${e}`:typeof t=="string"?`${a.localizedRouteNamePrefix}${e}-${t}`:e}createLocalizedRoute(e,t,a,o,s="",i,r=!1,n=!1,d,c){const h=this.buildRoutePathForLocales(t,e.path??"",encodeURI(s),o,i,r,c.defaultLocale.code);if(!h||h===e.path||t.length===0)return null;const u=t[0];if(!u)return null;const m=d??e.path??"",f=F(C(e.name??"",m),u,o,c.localizedRouteNamePrefix);return p(e,{path:h,name:f,children:this.createLocalizedChildren(a,m,t,!0,!1,n,{[u]:s},c),alias:[],meta:{alias:[]}})}buildLocalizedRouteName(e,t,a,o=!1,s){return a?o?`${s.localizedRouteNamePrefix}${e}-${t}`:t&&!I(t,s.defaultLocale,!1)?`${s.localizedRouteNamePrefix}${e}-${t}`:`${s.localizedRouteNamePrefix}${e}`:e}}function ie(l){switch(l){case"no_prefix":return new te;case"prefix":return new se;case"prefix_and_default":return new oe;case"prefix_except_default":return new le;default:throw new Error(`Unknown route strategy: ${l}`)}}class re{constructor(e){this.localizedPaths={};const{locales:t,defaultLocaleCode:a,strategy:o,globalLocaleRoutes:s,filesLocaleRoutes:i={},routeLocales:r={},noPrefixRedirect:n,excludePatterns:d,localizedRouteNamePrefix:c="localized-",customRegexMatcher:h,fallbackRedirectComponentPath:u}=e,m=Q(t,a);this.locales=m.locales,this.defaultLocale=m.defaultLocale,this.strategy=o,this.noPrefixRedirect=n,this.excludePatterns=d,this.localizedRouteNamePrefix=c,this.customRegex=h,this.fallbackRedirectComponentPath=u,this.activeLocaleCodes=this.computeActiveLocaleCodes();const f={};for(const P in s){const L=$(P),y=s[P];if(typeof y=="object"){const x={};for(const A in y){const v=y[A];v&&(x[A]=$(v))}f[L]=x}else f[L]=y}this.globalLocaleRoutes=f,this.rawGlobalLocaleRoutes=s??{},this.filesLocaleRoutes=i??{},this.routeLocales=r??{}}computeActiveLocaleCodes(){return this.locales.filter(e=>e.code!==this.defaultLocale.code||k.isPrefixAndDefaultStrategy(this.strategy)||k.isPrefixStrategy(this.strategy)).map(e=>e.code)}extendPages(e){const t=new Z({locales:this.locales,defaultLocaleCode:this.defaultLocale.code,strategy:this.strategy,globalLocaleRoutes:this.globalLocaleRoutes,filesLocaleRoutes:this.filesLocaleRoutes,routeLocales:this.routeLocales,pages:e,excludePatterns:this.excludePatterns,customRegex:this.customRegex,noPrefixRedirect:this.noPrefixRedirect,localizedRouteNamePrefix:this.localizedRouteNamePrefix,fallbackRedirectComponentPath:this.fallbackRedirectComponentPath,rawGlobalLocaleRoutes:this.rawGlobalLocaleRoutes});this.localizedPaths=t.localizedPaths;const a=ie(this.strategy),o=[...e],s=[];for(const r of o)s.push(...a.processPage(r,t));const i=a.postProcess(s,t);e.length=0,e.push(...i)}extractLocalizedPaths(e,t=""){return _(e,this.globalLocaleRoutes,this.filesLocaleRoutes,t)}ensureTranslationFilesExist(e,t,a,o){this.locales.forEach(s=>{const i=w.join(a,t,`${s.code}.json`);this.ensureFileExists(i),o||e.forEach(r=>{const n=w.join(a,t,"pages",`${r}/${s.code}.json`);this.ensureFileExists(n)})})}ensureFileExists(e){const t=w.dirname(e);S.existsSync(t)||S.mkdirSync(t,{recursive:!0}),S.existsSync(e)||S.writeFileSync(e,JSON.stringify({}),"utf-8")}resolveLocalizedPath(e,t){const a=$(e),o=g(a)||"/";let s;const i=this.globalLocaleRoutes[o],r=this.globalLocaleRoutes[a];i&&typeof i=="object"&&!Array.isArray(i)?s=i[t]:r&&typeof r=="object"&&!Array.isArray(r)&&(s=r[t]);const n=!!s,d=s?R(s):R(a)||"";let c=!1;if(this.strategy==="no_prefix"?c=!1:this.strategy==="prefix"||this.strategy==="prefix_and_default"?c=!0:this.strategy==="prefix_except_default"&&(c=t!==this.defaultLocale.code),t===this.defaultLocale.code&&!n&&(this.strategy==="prefix_except_default"||this.strategy==="no_prefix")&&(c=!1),!c)return R(d)||"/";const h=g(d);return h?R(`/${t}/${h}`):`/${t}`}generateDataRoutes(e,t,a){const o=[],s=e.map(i=>i.name).filter(i=>typeof i=="string"&&i.length>0);for(const i of this.locales)if(o.push(`/${t}/general/${i.code}/data.json`),!a)for(const r of s)o.push(`/${t}/${r}/${i.code}/data.json`);return o}}exports.RouteGenerator=re;exports.buildFullPath=N;exports.buildFullPathNoPrefix=M;exports.buildRouteName=F;exports.cloneArray=b;exports.extractLocalizedPaths=_;exports.isInternalPath=E;exports.isLocaleDefault=I;exports.isPageRedirectOnly=T;exports.normalizePath=R;exports.normalizeRouteKey=$;exports.removeLeadingSlash=g;exports.shouldAddLocalePrefix=V;
@@ -0,0 +1,4 @@
1
+ import { RouteGenerator, RouteGeneratorOptions } from './route-generator';
2
+ import { extractLocalizedPaths, LocalizedPathsMap } from './core/localized-paths';
3
+ import { isInternalPath, normalizeRouteKey, normalizePath, cloneArray, isPageRedirectOnly, removeLeadingSlash, buildRouteName, shouldAddLocalePrefix, isLocaleDefault, buildFullPath, buildFullPathNoPrefix } from './utils';
4
+ export { RouteGenerator, type RouteGeneratorOptions, extractLocalizedPaths, type LocalizedPathsMap, isInternalPath, normalizeRouteKey, normalizePath, cloneArray, isPageRedirectOnly, removeLeadingSlash, buildRouteName, shouldAddLocalePrefix, isLocaleDefault, buildFullPath, buildFullPathNoPrefix, };