@comvi/next 0.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 +21 -0
- package/README.md +256 -0
- package/dist/_virtual/_rolldown/runtime.cjs +23 -0
- package/dist/client/I18nProvider.cjs +101 -0
- package/dist/client/I18nProvider.d.ts +84 -0
- package/dist/client/I18nProvider.d.ts.map +1 -0
- package/dist/client/I18nProvider.js +99 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client.cjs +31 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +5 -0
- package/dist/createNextI18n.cjs +153 -0
- package/dist/createNextI18n.d.ts +183 -0
- package/dist/createNextI18n.d.ts.map +1 -0
- package/dist/createNextI18n.js +152 -0
- package/dist/index.cjs +17 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/middleware/createMiddleware.cjs +185 -0
- package/dist/middleware/createMiddleware.d.ts +38 -0
- package/dist/middleware/createMiddleware.d.ts.map +1 -0
- package/dist/middleware/createMiddleware.js +184 -0
- package/dist/middleware/types.d.ts +87 -0
- package/dist/middleware/types.d.ts.map +1 -0
- package/dist/middleware.cjs +3 -0
- package/dist/middleware.d.ts +3 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +2 -0
- package/dist/navigation.cjs +8 -0
- package/dist/navigation.d.ts +5 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +5 -0
- package/dist/routing/Link.cjs +42 -0
- package/dist/routing/Link.d.ts +25 -0
- package/dist/routing/Link.d.ts.map +1 -0
- package/dist/routing/Link.js +39 -0
- package/dist/routing/context.cjs +21 -0
- package/dist/routing/context.d.ts +9 -0
- package/dist/routing/context.d.ts.map +1 -0
- package/dist/routing/context.js +18 -0
- package/dist/routing/defineRouting.cjs +141 -0
- package/dist/routing/defineRouting.d.ts +123 -0
- package/dist/routing/defineRouting.d.ts.map +1 -0
- package/dist/routing/defineRouting.js +139 -0
- package/dist/routing/hooks.cjs +104 -0
- package/dist/routing/hooks.d.ts +66 -0
- package/dist/routing/hooks.d.ts.map +1 -0
- package/dist/routing/hooks.js +102 -0
- package/dist/routing/types.d.ts +35 -0
- package/dist/routing/types.d.ts.map +1 -0
- package/dist/routing/utils.cjs +94 -0
- package/dist/routing/utils.d.ts +8 -0
- package/dist/routing/utils.d.ts.map +1 -0
- package/dist/routing/utils.js +91 -0
- package/dist/routing.cjs +5 -0
- package/dist/routing.d.ts +4 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +2 -0
- package/dist/server/cache.cjs +69 -0
- package/dist/server/cache.d.ts +42 -0
- package/dist/server/cache.d.ts.map +1 -0
- package/dist/server/cache.js +66 -0
- package/dist/server/ensureInitialized.cjs +19 -0
- package/dist/server/ensureInitialized.d.ts +7 -0
- package/dist/server/ensureInitialized.d.ts.map +1 -0
- package/dist/server/ensureInitialized.js +19 -0
- package/dist/server/getI18n.cjs +115 -0
- package/dist/server/getI18n.d.ts +61 -0
- package/dist/server/getI18n.d.ts.map +1 -0
- package/dist/server/getI18n.js +114 -0
- package/dist/server/getLocale.cjs +37 -0
- package/dist/server/getLocale.d.ts +22 -0
- package/dist/server/getLocale.d.ts.map +1 -0
- package/dist/server/getLocale.js +36 -0
- package/dist/server/loadTranslations.cjs +54 -0
- package/dist/server/loadTranslations.d.ts +34 -0
- package/dist/server/loadTranslations.d.ts.map +1 -0
- package/dist/server/loadTranslations.js +54 -0
- package/dist/server/setRequestLocale.cjs +31 -0
- package/dist/server/setRequestLocale.d.ts +26 -0
- package/dist/server/setRequestLocale.d.ts.map +1 -0
- package/dist/server/setRequestLocale.js +31 -0
- package/dist/server/types.d.ts +43 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server.cjs +11 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +6 -0
- package/package.json +111 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
require("../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_defineRouting = require("../routing/defineRouting.cjs");
|
|
3
|
+
const require_utils = require("../routing/utils.cjs");
|
|
4
|
+
let next_server = require("next/server");
|
|
5
|
+
//#region src/middleware/createMiddleware.ts
|
|
6
|
+
/**
|
|
7
|
+
* Header name for passing locale to Server Components
|
|
8
|
+
*/
|
|
9
|
+
var LOCALE_HEADER = "x-comvi-locale";
|
|
10
|
+
/**
|
|
11
|
+
* Default detection order
|
|
12
|
+
*/
|
|
13
|
+
var DEFAULT_DETECTION_ORDER = ["cookie", "accept-language"];
|
|
14
|
+
/**
|
|
15
|
+
* Creates i18n middleware for Next.js
|
|
16
|
+
*
|
|
17
|
+
* The middleware handles:
|
|
18
|
+
* - Locale detection from URL, cookie, header, and Accept-Language
|
|
19
|
+
* - Locale persistence via cookie
|
|
20
|
+
* - URL prefix handling based on localePrefix mode
|
|
21
|
+
* - Setting x-comvi-locale header for Server Components
|
|
22
|
+
*
|
|
23
|
+
* @param config - Middleware configuration
|
|
24
|
+
* @returns Next.js middleware function
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* // middleware.ts - Basic usage
|
|
29
|
+
* import { createMiddleware } from '@comvi/next/middleware';
|
|
30
|
+
* import { routing } from './i18n/routing';
|
|
31
|
+
*
|
|
32
|
+
* export default createMiddleware(routing);
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* // middleware.ts - Custom detection order
|
|
38
|
+
* export default createMiddleware({
|
|
39
|
+
* ...routing,
|
|
40
|
+
* localeDetection: {
|
|
41
|
+
* order: ['header', 'cookie', 'accept-language'],
|
|
42
|
+
* headerName: 'x-user-locale',
|
|
43
|
+
* cookieName: 'MY_LOCALE',
|
|
44
|
+
* },
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
function createMiddleware(config) {
|
|
49
|
+
const { locales, defaultLocale, localePrefix = "as-needed", localeCookie = "NEXT_LOCALE", detectLocale: customDetector, localeDetection } = config;
|
|
50
|
+
const detectionOrder = localeDetection?.order ?? DEFAULT_DETECTION_ORDER;
|
|
51
|
+
const cookieName = localeDetection?.cookieName ?? localeCookie;
|
|
52
|
+
const cookieSecureConfig = localeDetection?.cookieSecure ?? true;
|
|
53
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
54
|
+
const headerName = localeDetection?.headerName;
|
|
55
|
+
const resolveAcceptLanguage = localeDetection?.resolveAcceptLanguage ?? defaultResolveAcceptLanguage;
|
|
56
|
+
const routing = {
|
|
57
|
+
locales,
|
|
58
|
+
defaultLocale,
|
|
59
|
+
localePrefix,
|
|
60
|
+
localeCookie,
|
|
61
|
+
pathnames: config.pathnames ?? {}
|
|
62
|
+
};
|
|
63
|
+
const getPathname = require_defineRouting.createGetPathname(routing);
|
|
64
|
+
return function middleware(request) {
|
|
65
|
+
const { pathname } = request.nextUrl;
|
|
66
|
+
if (shouldSkipPath(pathname)) return next_server.NextResponse.next();
|
|
67
|
+
const pathLocale = extractLocaleFromPath(pathname, locales);
|
|
68
|
+
let detectedLocale;
|
|
69
|
+
if (customDetector) detectedLocale = customDetector(request);
|
|
70
|
+
if (!detectedLocale && pathLocale) detectedLocale = pathLocale;
|
|
71
|
+
if (!detectedLocale) for (const source of detectionOrder) {
|
|
72
|
+
const detected = detectFromSource(request, source, locales, defaultLocale, cookieName, headerName, resolveAcceptLanguage);
|
|
73
|
+
if (detected) {
|
|
74
|
+
detectedLocale = detected;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const locale = detectedLocale && locales.includes(detectedLocale) ? detectedLocale : defaultLocale;
|
|
79
|
+
const requestHeaders = new Headers(request.headers);
|
|
80
|
+
requestHeaders.set(LOCALE_HEADER, locale);
|
|
81
|
+
const canonicalPathname = require_utils.getCanonicalPathname(require_utils.stripLocalePrefix(pathname, locales), routing, locale);
|
|
82
|
+
const publicPathname = getPathname({
|
|
83
|
+
locale,
|
|
84
|
+
href: canonicalPathname
|
|
85
|
+
});
|
|
86
|
+
let response;
|
|
87
|
+
if (pathname !== publicPathname) {
|
|
88
|
+
const url = request.nextUrl.clone();
|
|
89
|
+
url.pathname = publicPathname;
|
|
90
|
+
response = next_server.NextResponse.redirect(url);
|
|
91
|
+
} else {
|
|
92
|
+
const internalPathname = getInternalPathname(locale, canonicalPathname);
|
|
93
|
+
const middlewareInit = { request: { headers: requestHeaders } };
|
|
94
|
+
if (pathname === internalPathname) response = next_server.NextResponse.next(middlewareInit);
|
|
95
|
+
else {
|
|
96
|
+
const url = request.nextUrl.clone();
|
|
97
|
+
url.pathname = internalPathname;
|
|
98
|
+
response = next_server.NextResponse.rewrite(url, middlewareInit);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
response.headers.set(LOCALE_HEADER, locale);
|
|
102
|
+
if (request.cookies.get(cookieName)?.value !== locale) response.cookies.set(cookieName, locale, {
|
|
103
|
+
path: "/",
|
|
104
|
+
maxAge: 365 * 24 * 60 * 60,
|
|
105
|
+
sameSite: "lax",
|
|
106
|
+
secure: isDev ? false : cookieSecureConfig
|
|
107
|
+
});
|
|
108
|
+
return response;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Detect locale from a specific source
|
|
113
|
+
*/
|
|
114
|
+
function detectFromSource(request, source, locales, defaultLocale, cookieName, headerName, resolveAcceptLanguage) {
|
|
115
|
+
switch (source) {
|
|
116
|
+
case "cookie": {
|
|
117
|
+
const cookieValue = request.cookies.get(cookieName)?.value;
|
|
118
|
+
if (cookieValue && locales.includes(cookieValue)) return cookieValue;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
case "header": {
|
|
122
|
+
if (!headerName) return void 0;
|
|
123
|
+
const headerValue = request.headers.get(headerName);
|
|
124
|
+
if (headerValue && locales.includes(headerValue)) return headerValue;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
case "accept-language": {
|
|
128
|
+
const acceptLanguage = request.headers.get("accept-language");
|
|
129
|
+
if (!acceptLanguage) return void 0;
|
|
130
|
+
return resolveAcceptLanguage(acceptLanguage, locales, defaultLocale);
|
|
131
|
+
}
|
|
132
|
+
default: return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Check if path should be skipped by middleware.
|
|
137
|
+
* Static asset filtering is expected to be handled by Next.js matcher config.
|
|
138
|
+
*/
|
|
139
|
+
function shouldSkipPath(pathname) {
|
|
140
|
+
const isApiPath = pathname === "/api" || pathname.startsWith("/api/");
|
|
141
|
+
return pathname.startsWith("/_next") || isApiPath;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Extract locale from URL path
|
|
145
|
+
*/
|
|
146
|
+
function extractLocaleFromPath(pathname, locales) {
|
|
147
|
+
const firstSegment = pathname.split("/").filter(Boolean)[0];
|
|
148
|
+
return locales.includes(firstSegment) ? firstSegment : void 0;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Default Accept-Language resolver
|
|
152
|
+
*
|
|
153
|
+
* Handles common cases with a simple parser. For production apps with
|
|
154
|
+
* diverse locales (CJK, regional variants), use @formatjs/intl-localematcher.
|
|
155
|
+
*
|
|
156
|
+
* Matching strategy:
|
|
157
|
+
* 1. Try exact match (e.g., "en-US" matches "en-US")
|
|
158
|
+
* 2. Try base language match (e.g., "en-US" matches "en")
|
|
159
|
+
* 3. Try finding a locale that starts with the language (e.g., "en" matches "en-US")
|
|
160
|
+
*/
|
|
161
|
+
function defaultResolveAcceptLanguage(acceptLanguage, locales, _defaultLocale) {
|
|
162
|
+
const languages = acceptLanguage.split(",").map((lang) => {
|
|
163
|
+
const [code, q = "q=1"] = lang.trim().split(";");
|
|
164
|
+
const quality = parseFloat(q.split("=")[1] || "1");
|
|
165
|
+
return {
|
|
166
|
+
code: code.trim(),
|
|
167
|
+
quality: isNaN(quality) ? 1 : quality
|
|
168
|
+
};
|
|
169
|
+
}).filter(({ code, quality }) => code.length > 0 && quality > 0).sort((a, b) => b.quality - a.quality);
|
|
170
|
+
for (const { code } of languages) {
|
|
171
|
+
const normalizedCode = code.toLowerCase();
|
|
172
|
+
const exactMatch = locales.find((locale) => locale.toLowerCase() === normalizedCode);
|
|
173
|
+
if (exactMatch) return exactMatch;
|
|
174
|
+
const baseLang = normalizedCode.split("-")[0];
|
|
175
|
+
const baseMatch = locales.find((locale) => locale.toLowerCase() === baseLang);
|
|
176
|
+
if (baseMatch) return baseMatch;
|
|
177
|
+
const prefixMatch = locales.find((locale) => locale.toLowerCase().startsWith(baseLang + "-"));
|
|
178
|
+
if (prefixMatch) return prefixMatch;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function getInternalPathname(locale, canonicalPathname) {
|
|
182
|
+
return canonicalPathname === "/" ? `/${locale}` : `/${locale}${canonicalPathname}`;
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
exports.createMiddleware = createMiddleware;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { MiddlewareConfig } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Creates i18n middleware for Next.js
|
|
5
|
+
*
|
|
6
|
+
* The middleware handles:
|
|
7
|
+
* - Locale detection from URL, cookie, header, and Accept-Language
|
|
8
|
+
* - Locale persistence via cookie
|
|
9
|
+
* - URL prefix handling based on localePrefix mode
|
|
10
|
+
* - Setting x-comvi-locale header for Server Components
|
|
11
|
+
*
|
|
12
|
+
* @param config - Middleware configuration
|
|
13
|
+
* @returns Next.js middleware function
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* // middleware.ts - Basic usage
|
|
18
|
+
* import { createMiddleware } from '@comvi/next/middleware';
|
|
19
|
+
* import { routing } from './i18n/routing';
|
|
20
|
+
*
|
|
21
|
+
* export default createMiddleware(routing);
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* // middleware.ts - Custom detection order
|
|
27
|
+
* export default createMiddleware({
|
|
28
|
+
* ...routing,
|
|
29
|
+
* localeDetection: {
|
|
30
|
+
* order: ['header', 'cookie', 'accept-language'],
|
|
31
|
+
* headerName: 'x-user-locale',
|
|
32
|
+
* cookieName: 'MY_LOCALE',
|
|
33
|
+
* },
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function createMiddleware(config: MiddlewareConfig): (request: NextRequest) => NextResponse;
|
|
38
|
+
//# sourceMappingURL=createMiddleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createMiddleware.d.ts","sourceRoot":"","sources":["../../src/middleware/createMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,KAAK,EAAE,gBAAgB,EAAyB,MAAM,SAAS,CAAC;AAevE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,IA2B5B,SAAS,WAAW,KAAG,YAAY,CA6F/D"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { createGetPathname } from "../routing/defineRouting.js";
|
|
2
|
+
import { getCanonicalPathname, stripLocalePrefix } from "../routing/utils.js";
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
4
|
+
//#region src/middleware/createMiddleware.ts
|
|
5
|
+
/**
|
|
6
|
+
* Header name for passing locale to Server Components
|
|
7
|
+
*/
|
|
8
|
+
var LOCALE_HEADER = "x-comvi-locale";
|
|
9
|
+
/**
|
|
10
|
+
* Default detection order
|
|
11
|
+
*/
|
|
12
|
+
var DEFAULT_DETECTION_ORDER = ["cookie", "accept-language"];
|
|
13
|
+
/**
|
|
14
|
+
* Creates i18n middleware for Next.js
|
|
15
|
+
*
|
|
16
|
+
* The middleware handles:
|
|
17
|
+
* - Locale detection from URL, cookie, header, and Accept-Language
|
|
18
|
+
* - Locale persistence via cookie
|
|
19
|
+
* - URL prefix handling based on localePrefix mode
|
|
20
|
+
* - Setting x-comvi-locale header for Server Components
|
|
21
|
+
*
|
|
22
|
+
* @param config - Middleware configuration
|
|
23
|
+
* @returns Next.js middleware function
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* // middleware.ts - Basic usage
|
|
28
|
+
* import { createMiddleware } from '@comvi/next/middleware';
|
|
29
|
+
* import { routing } from './i18n/routing';
|
|
30
|
+
*
|
|
31
|
+
* export default createMiddleware(routing);
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* // middleware.ts - Custom detection order
|
|
37
|
+
* export default createMiddleware({
|
|
38
|
+
* ...routing,
|
|
39
|
+
* localeDetection: {
|
|
40
|
+
* order: ['header', 'cookie', 'accept-language'],
|
|
41
|
+
* headerName: 'x-user-locale',
|
|
42
|
+
* cookieName: 'MY_LOCALE',
|
|
43
|
+
* },
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
function createMiddleware(config) {
|
|
48
|
+
const { locales, defaultLocale, localePrefix = "as-needed", localeCookie = "NEXT_LOCALE", detectLocale: customDetector, localeDetection } = config;
|
|
49
|
+
const detectionOrder = localeDetection?.order ?? DEFAULT_DETECTION_ORDER;
|
|
50
|
+
const cookieName = localeDetection?.cookieName ?? localeCookie;
|
|
51
|
+
const cookieSecureConfig = localeDetection?.cookieSecure ?? true;
|
|
52
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
53
|
+
const headerName = localeDetection?.headerName;
|
|
54
|
+
const resolveAcceptLanguage = localeDetection?.resolveAcceptLanguage ?? defaultResolveAcceptLanguage;
|
|
55
|
+
const routing = {
|
|
56
|
+
locales,
|
|
57
|
+
defaultLocale,
|
|
58
|
+
localePrefix,
|
|
59
|
+
localeCookie,
|
|
60
|
+
pathnames: config.pathnames ?? {}
|
|
61
|
+
};
|
|
62
|
+
const getPathname = createGetPathname(routing);
|
|
63
|
+
return function middleware(request) {
|
|
64
|
+
const { pathname } = request.nextUrl;
|
|
65
|
+
if (shouldSkipPath(pathname)) return NextResponse.next();
|
|
66
|
+
const pathLocale = extractLocaleFromPath(pathname, locales);
|
|
67
|
+
let detectedLocale;
|
|
68
|
+
if (customDetector) detectedLocale = customDetector(request);
|
|
69
|
+
if (!detectedLocale && pathLocale) detectedLocale = pathLocale;
|
|
70
|
+
if (!detectedLocale) for (const source of detectionOrder) {
|
|
71
|
+
const detected = detectFromSource(request, source, locales, defaultLocale, cookieName, headerName, resolveAcceptLanguage);
|
|
72
|
+
if (detected) {
|
|
73
|
+
detectedLocale = detected;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const locale = detectedLocale && locales.includes(detectedLocale) ? detectedLocale : defaultLocale;
|
|
78
|
+
const requestHeaders = new Headers(request.headers);
|
|
79
|
+
requestHeaders.set(LOCALE_HEADER, locale);
|
|
80
|
+
const canonicalPathname = getCanonicalPathname(stripLocalePrefix(pathname, locales), routing, locale);
|
|
81
|
+
const publicPathname = getPathname({
|
|
82
|
+
locale,
|
|
83
|
+
href: canonicalPathname
|
|
84
|
+
});
|
|
85
|
+
let response;
|
|
86
|
+
if (pathname !== publicPathname) {
|
|
87
|
+
const url = request.nextUrl.clone();
|
|
88
|
+
url.pathname = publicPathname;
|
|
89
|
+
response = NextResponse.redirect(url);
|
|
90
|
+
} else {
|
|
91
|
+
const internalPathname = getInternalPathname(locale, canonicalPathname);
|
|
92
|
+
const middlewareInit = { request: { headers: requestHeaders } };
|
|
93
|
+
if (pathname === internalPathname) response = NextResponse.next(middlewareInit);
|
|
94
|
+
else {
|
|
95
|
+
const url = request.nextUrl.clone();
|
|
96
|
+
url.pathname = internalPathname;
|
|
97
|
+
response = NextResponse.rewrite(url, middlewareInit);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
response.headers.set(LOCALE_HEADER, locale);
|
|
101
|
+
if (request.cookies.get(cookieName)?.value !== locale) response.cookies.set(cookieName, locale, {
|
|
102
|
+
path: "/",
|
|
103
|
+
maxAge: 365 * 24 * 60 * 60,
|
|
104
|
+
sameSite: "lax",
|
|
105
|
+
secure: isDev ? false : cookieSecureConfig
|
|
106
|
+
});
|
|
107
|
+
return response;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Detect locale from a specific source
|
|
112
|
+
*/
|
|
113
|
+
function detectFromSource(request, source, locales, defaultLocale, cookieName, headerName, resolveAcceptLanguage) {
|
|
114
|
+
switch (source) {
|
|
115
|
+
case "cookie": {
|
|
116
|
+
const cookieValue = request.cookies.get(cookieName)?.value;
|
|
117
|
+
if (cookieValue && locales.includes(cookieValue)) return cookieValue;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
case "header": {
|
|
121
|
+
if (!headerName) return void 0;
|
|
122
|
+
const headerValue = request.headers.get(headerName);
|
|
123
|
+
if (headerValue && locales.includes(headerValue)) return headerValue;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
case "accept-language": {
|
|
127
|
+
const acceptLanguage = request.headers.get("accept-language");
|
|
128
|
+
if (!acceptLanguage) return void 0;
|
|
129
|
+
return resolveAcceptLanguage(acceptLanguage, locales, defaultLocale);
|
|
130
|
+
}
|
|
131
|
+
default: return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Check if path should be skipped by middleware.
|
|
136
|
+
* Static asset filtering is expected to be handled by Next.js matcher config.
|
|
137
|
+
*/
|
|
138
|
+
function shouldSkipPath(pathname) {
|
|
139
|
+
const isApiPath = pathname === "/api" || pathname.startsWith("/api/");
|
|
140
|
+
return pathname.startsWith("/_next") || isApiPath;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Extract locale from URL path
|
|
144
|
+
*/
|
|
145
|
+
function extractLocaleFromPath(pathname, locales) {
|
|
146
|
+
const firstSegment = pathname.split("/").filter(Boolean)[0];
|
|
147
|
+
return locales.includes(firstSegment) ? firstSegment : void 0;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Default Accept-Language resolver
|
|
151
|
+
*
|
|
152
|
+
* Handles common cases with a simple parser. For production apps with
|
|
153
|
+
* diverse locales (CJK, regional variants), use @formatjs/intl-localematcher.
|
|
154
|
+
*
|
|
155
|
+
* Matching strategy:
|
|
156
|
+
* 1. Try exact match (e.g., "en-US" matches "en-US")
|
|
157
|
+
* 2. Try base language match (e.g., "en-US" matches "en")
|
|
158
|
+
* 3. Try finding a locale that starts with the language (e.g., "en" matches "en-US")
|
|
159
|
+
*/
|
|
160
|
+
function defaultResolveAcceptLanguage(acceptLanguage, locales, _defaultLocale) {
|
|
161
|
+
const languages = acceptLanguage.split(",").map((lang) => {
|
|
162
|
+
const [code, q = "q=1"] = lang.trim().split(";");
|
|
163
|
+
const quality = parseFloat(q.split("=")[1] || "1");
|
|
164
|
+
return {
|
|
165
|
+
code: code.trim(),
|
|
166
|
+
quality: isNaN(quality) ? 1 : quality
|
|
167
|
+
};
|
|
168
|
+
}).filter(({ code, quality }) => code.length > 0 && quality > 0).sort((a, b) => b.quality - a.quality);
|
|
169
|
+
for (const { code } of languages) {
|
|
170
|
+
const normalizedCode = code.toLowerCase();
|
|
171
|
+
const exactMatch = locales.find((locale) => locale.toLowerCase() === normalizedCode);
|
|
172
|
+
if (exactMatch) return exactMatch;
|
|
173
|
+
const baseLang = normalizedCode.split("-")[0];
|
|
174
|
+
const baseMatch = locales.find((locale) => locale.toLowerCase() === baseLang);
|
|
175
|
+
if (baseMatch) return baseMatch;
|
|
176
|
+
const prefixMatch = locales.find((locale) => locale.toLowerCase().startsWith(baseLang + "-"));
|
|
177
|
+
if (prefixMatch) return prefixMatch;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function getInternalPathname(locale, canonicalPathname) {
|
|
181
|
+
return canonicalPathname === "/" ? `/${locale}` : `/${locale}${canonicalPathname}`;
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
export { createMiddleware };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { RoutingConfig } from '../routing/types';
|
|
3
|
+
/**
|
|
4
|
+
* Locale detection source types
|
|
5
|
+
*/
|
|
6
|
+
export type LocaleDetectionSource = "cookie" | "header" | "accept-language";
|
|
7
|
+
/**
|
|
8
|
+
* Function type for custom Accept-Language resolution
|
|
9
|
+
*
|
|
10
|
+
* @param acceptLanguage - The Accept-Language header value
|
|
11
|
+
* @param locales - Array of supported locales
|
|
12
|
+
* @param defaultLocale - The default locale to fall back to
|
|
13
|
+
* @returns The matched locale or undefined
|
|
14
|
+
*
|
|
15
|
+
* @example Using @formatjs/intl-localematcher (recommended for production)
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import Negotiator from 'negotiator';
|
|
18
|
+
* import { match } from '@formatjs/intl-localematcher';
|
|
19
|
+
*
|
|
20
|
+
* const resolveAcceptLanguage = (header, locales, defaultLocale) => {
|
|
21
|
+
* try {
|
|
22
|
+
* const languages = new Negotiator({
|
|
23
|
+
* headers: { 'accept-language': header }
|
|
24
|
+
* }).languages();
|
|
25
|
+
* return match(languages, [...locales], defaultLocale);
|
|
26
|
+
* } catch {
|
|
27
|
+
* return undefined;
|
|
28
|
+
* }
|
|
29
|
+
* };
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export type ResolveAcceptLanguage = (acceptLanguage: string, locales: readonly string[], defaultLocale: string) => string | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Configuration for locale detection
|
|
35
|
+
*/
|
|
36
|
+
export interface LocaleDetectionConfig {
|
|
37
|
+
/**
|
|
38
|
+
* Detection order (URL path is always checked first)
|
|
39
|
+
* @default ['cookie', 'accept-language']
|
|
40
|
+
*/
|
|
41
|
+
order?: LocaleDetectionSource[];
|
|
42
|
+
/**
|
|
43
|
+
* Cookie name for storing locale preference
|
|
44
|
+
* @default 'NEXT_LOCALE'
|
|
45
|
+
*/
|
|
46
|
+
cookieName?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Set the Secure flag on the locale cookie.
|
|
49
|
+
* When true, the cookie is only sent over HTTPS.
|
|
50
|
+
* Automatically disabled in development (NODE_ENV=development) regardless of this setting.
|
|
51
|
+
* @default true
|
|
52
|
+
*/
|
|
53
|
+
cookieSecure?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Custom header name to read locale from
|
|
56
|
+
* Useful when you have a proxy that sets locale header
|
|
57
|
+
* @default undefined (not checked unless in order)
|
|
58
|
+
*/
|
|
59
|
+
headerName?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Custom Accept-Language resolution function (pluggable)
|
|
62
|
+
*
|
|
63
|
+
* By default, uses a simple parser that handles common cases.
|
|
64
|
+
* For production apps with diverse locales (especially CJK languages),
|
|
65
|
+
* consider using @formatjs/intl-localematcher for RFC-compliant matching.
|
|
66
|
+
*
|
|
67
|
+
* @default Built-in simple parser
|
|
68
|
+
*/
|
|
69
|
+
resolveAcceptLanguage?: ResolveAcceptLanguage;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Configuration for i18n middleware
|
|
73
|
+
*/
|
|
74
|
+
export interface MiddlewareConfig extends RoutingConfig {
|
|
75
|
+
/**
|
|
76
|
+
* Custom locale detection logic (optional)
|
|
77
|
+
* If provided, this function is called first to detect locale
|
|
78
|
+
* Return undefined to fall back to default detection
|
|
79
|
+
*/
|
|
80
|
+
detectLocale?: (request: NextRequest) => string | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Locale detection configuration
|
|
83
|
+
* Allows customizing detection sources and their order
|
|
84
|
+
*/
|
|
85
|
+
localeDetection?: LocaleDetectionConfig;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/middleware/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,QAAQ,GAAG,iBAAiB,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAClC,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,aAAa,EAAE,MAAM,KAClB,MAAM,GAAG,SAAS,CAAC;AAExB;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,KAAK,CAAC,EAAE,qBAAqB,EAAE,CAAC;IAEhC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;;;OAQG;IACH,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;CAC/C;AAED;;GAEG;AACH,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACrD;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,GAAG,SAAS,CAAC;IAE5D;;;OAGG;IACH,eAAe,CAAC,EAAE,qBAAqB,CAAC;CACzC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,YAAY,EACV,gBAAgB,EAChB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
4
|
+
const require_Link = require("./routing/Link.cjs");
|
|
5
|
+
const require_hooks = require("./routing/hooks.cjs");
|
|
6
|
+
exports.Link = require_Link.Link;
|
|
7
|
+
exports.useLocalizedRouter = require_hooks.useLocalizedRouter;
|
|
8
|
+
exports.usePathname = require_hooks.usePathname;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAClE,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
4
|
+
const require_context = require("./context.cjs");
|
|
5
|
+
const require_utils = require("./utils.cjs");
|
|
6
|
+
let react = require("react");
|
|
7
|
+
react = require_runtime.__toESM(react);
|
|
8
|
+
let _comvi_react = require("@comvi/react");
|
|
9
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
let next_link = require("next/link");
|
|
11
|
+
next_link = require_runtime.__toESM(next_link);
|
|
12
|
+
//#region src/routing/Link.tsx
|
|
13
|
+
/**
|
|
14
|
+
* Localized Link component that adds locale prefix based on routing config
|
|
15
|
+
*
|
|
16
|
+
* This component wraps Next.js Link and prepends the current locale
|
|
17
|
+
* using localePrefix/pathnames rules from the routing configuration.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* import { Link } from '@comvi/next/navigation';
|
|
22
|
+
*
|
|
23
|
+
* // Uses current locale
|
|
24
|
+
* <Link href="/about">About</Link>
|
|
25
|
+
*
|
|
26
|
+
* // Specify different locale
|
|
27
|
+
* <Link href="/about" locale="de">German About</Link>
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
var Link = (0, react.forwardRef)(function Link({ href, locale: targetLocale, ...props }, ref) {
|
|
31
|
+
const { locale: currentLocale } = (0, _comvi_react.useI18n)();
|
|
32
|
+
const locale = targetLocale ?? currentLocale;
|
|
33
|
+
const routing = require_context.useRoutingConfig();
|
|
34
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(next_link.default, {
|
|
35
|
+
ref,
|
|
36
|
+
href: typeof href === "string" ? require_utils.localizeHref(href, locale, routing ?? void 0) : require_utils.localizeUrlObject(href, locale, routing ?? void 0),
|
|
37
|
+
...props
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
Link.displayName = "LocalizedLink";
|
|
41
|
+
//#endregion
|
|
42
|
+
exports.Link = Link;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { default as NextLink } from 'next/link';
|
|
2
|
+
import { default as React, ComponentProps } from 'react';
|
|
3
|
+
export interface LocalizedLinkProps extends Omit<ComponentProps<typeof NextLink>, "locale"> {
|
|
4
|
+
/** Target locale (defaults to current locale) */
|
|
5
|
+
locale?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Localized Link component that adds locale prefix based on routing config
|
|
9
|
+
*
|
|
10
|
+
* This component wraps Next.js Link and prepends the current locale
|
|
11
|
+
* using localePrefix/pathnames rules from the routing configuration.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* import { Link } from '@comvi/next/navigation';
|
|
16
|
+
*
|
|
17
|
+
* // Uses current locale
|
|
18
|
+
* <Link href="/about">About</Link>
|
|
19
|
+
*
|
|
20
|
+
* // Specify different locale
|
|
21
|
+
* <Link href="/about" locale="de">German About</Link>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare const Link: React.ForwardRefExoticComponent<Omit<LocalizedLinkProps, "ref"> & React.RefAttributes<HTMLAnchorElement>>;
|
|
25
|
+
//# sourceMappingURL=Link.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Link.d.ts","sourceRoot":"","sources":["../../src/routing/Link.tsx"],"names":[],"mappings":"AAEA,OAAO,QAAQ,MAAM,WAAW,CAAC;AAEjC,OAAO,KAAqB,MAAM,OAAO,CAAC;AAC1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAI5C,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,cAAc,CAAC,OAAO,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACzF,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,IAAI,2GAef,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
import { useRoutingConfig } from "./context.js";
|
|
4
|
+
import { localizeHref, localizeUrlObject } from "./utils.js";
|
|
5
|
+
import { forwardRef } from "react";
|
|
6
|
+
import { useI18n } from "@comvi/react";
|
|
7
|
+
import { jsx } from "react/jsx-runtime";
|
|
8
|
+
import NextLink from "next/link";
|
|
9
|
+
//#region src/routing/Link.tsx
|
|
10
|
+
/**
|
|
11
|
+
* Localized Link component that adds locale prefix based on routing config
|
|
12
|
+
*
|
|
13
|
+
* This component wraps Next.js Link and prepends the current locale
|
|
14
|
+
* using localePrefix/pathnames rules from the routing configuration.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* import { Link } from '@comvi/next/navigation';
|
|
19
|
+
*
|
|
20
|
+
* // Uses current locale
|
|
21
|
+
* <Link href="/about">About</Link>
|
|
22
|
+
*
|
|
23
|
+
* // Specify different locale
|
|
24
|
+
* <Link href="/about" locale="de">German About</Link>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
var Link = forwardRef(function Link({ href, locale: targetLocale, ...props }, ref) {
|
|
28
|
+
const { locale: currentLocale } = useI18n();
|
|
29
|
+
const locale = targetLocale ?? currentLocale;
|
|
30
|
+
const routing = useRoutingConfig();
|
|
31
|
+
return /* @__PURE__ */ jsx(NextLink, {
|
|
32
|
+
ref,
|
|
33
|
+
href: typeof href === "string" ? localizeHref(href, locale, routing ?? void 0) : localizeUrlObject(href, locale, routing ?? void 0),
|
|
34
|
+
...props
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
Link.displayName = "LocalizedLink";
|
|
38
|
+
//#endregion
|
|
39
|
+
export { Link };
|