@better-i18n/next 0.2.4 → 0.4.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/package.json +2 -2
- package/src/index.ts +45 -1
- package/src/middleware.ts +114 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-i18n/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Better-i18n Next.js integration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"typecheck": "tsc --noEmit"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@better-i18n/core": "0.1.
|
|
60
|
+
"@better-i18n/core": "0.1.6"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"next": ">=15.0.0",
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
createI18nProxy,
|
|
10
10
|
createBetterI18nMiddleware,
|
|
11
11
|
composeMiddleware,
|
|
12
|
+
type MiddlewareCallback,
|
|
12
13
|
} from "./middleware";
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -22,13 +23,21 @@ import {
|
|
|
22
23
|
* export const i18n = createI18n({
|
|
23
24
|
* project: 'acme/dashboard',
|
|
24
25
|
* defaultLocale: 'en',
|
|
26
|
+
* localePrefix: 'always',
|
|
25
27
|
* })
|
|
26
28
|
*
|
|
27
29
|
* // i18n/request.ts
|
|
28
30
|
* export default i18n.requestConfig
|
|
29
31
|
*
|
|
30
|
-
* // middleware.ts
|
|
32
|
+
* // middleware.ts (simple)
|
|
31
33
|
* export default i18n.middleware
|
|
34
|
+
*
|
|
35
|
+
* // middleware.ts (with auth - Clerk-style)
|
|
36
|
+
* export default i18n.betterMiddleware(async (request, { locale }) => {
|
|
37
|
+
* if (needsLogin) {
|
|
38
|
+
* return NextResponse.redirect(new URL(`/${locale}/login`, request.url));
|
|
39
|
+
* }
|
|
40
|
+
* });
|
|
32
41
|
* ```
|
|
33
42
|
*/
|
|
34
43
|
export const createI18n = (config: I18nConfig) => {
|
|
@@ -38,16 +47,51 @@ export const createI18n = (config: I18nConfig) => {
|
|
|
38
47
|
return {
|
|
39
48
|
config: normalized,
|
|
40
49
|
requestConfig: createNextIntlRequestConfig(normalized),
|
|
50
|
+
/** @deprecated Use betterMiddleware() for Clerk-style callback support */
|
|
41
51
|
middleware: createI18nMiddleware(normalized),
|
|
42
52
|
proxy: createI18nProxy(normalized),
|
|
43
53
|
getManifest: i18n.getManifest,
|
|
44
54
|
getLocales: i18n.getLocales,
|
|
45
55
|
getMessages: i18n.getMessages,
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create middleware with optional Clerk-style callback for auth integration
|
|
59
|
+
*
|
|
60
|
+
* @example Simple usage
|
|
61
|
+
* ```ts
|
|
62
|
+
* export default i18n.betterMiddleware();
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @example With auth callback
|
|
66
|
+
* ```ts
|
|
67
|
+
* export default i18n.betterMiddleware(async (request, { locale, response }) => {
|
|
68
|
+
* if (needsLogin) {
|
|
69
|
+
* return NextResponse.redirect(new URL(`/${locale}/login`, request.url));
|
|
70
|
+
* }
|
|
71
|
+
* // Return nothing = i18n response is used (headers preserved!)
|
|
72
|
+
* });
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
betterMiddleware: (callback?: MiddlewareCallback) => {
|
|
76
|
+
return createBetterI18nMiddleware(
|
|
77
|
+
{
|
|
78
|
+
project: normalized.project,
|
|
79
|
+
defaultLocale: normalized.defaultLocale,
|
|
80
|
+
localePrefix: normalized.localePrefix,
|
|
81
|
+
detection: {
|
|
82
|
+
cookie: true,
|
|
83
|
+
browserLanguage: true,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
callback
|
|
87
|
+
);
|
|
88
|
+
},
|
|
46
89
|
};
|
|
47
90
|
};
|
|
48
91
|
|
|
49
92
|
// Modern standalone middleware exports
|
|
50
93
|
export { createBetterI18nMiddleware, composeMiddleware };
|
|
94
|
+
export type { MiddlewareContext, MiddlewareCallback } from "./middleware";
|
|
51
95
|
|
|
52
96
|
// Core instance factory
|
|
53
97
|
export { createNextI18nCore } from "./server";
|
package/src/middleware.ts
CHANGED
|
@@ -7,6 +7,28 @@ import { normalizeConfig } from "./config";
|
|
|
7
7
|
import { getLocales } from "./server";
|
|
8
8
|
import type { I18nConfig } from "./types";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Context passed to the middleware callback (Clerk-style pattern)
|
|
12
|
+
*/
|
|
13
|
+
export interface MiddlewareContext {
|
|
14
|
+
/** Detected locale from the request */
|
|
15
|
+
locale: string;
|
|
16
|
+
/** The i18n response with headers already set - can be modified */
|
|
17
|
+
response: NextResponse;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Callback function for Clerk-style middleware composition
|
|
22
|
+
*
|
|
23
|
+
* @param request - The incoming Next.js request
|
|
24
|
+
* @param context - Contains locale and response from i18n middleware
|
|
25
|
+
* @returns NextResponse to short-circuit (e.g., redirect), or void to continue with i18n response
|
|
26
|
+
*/
|
|
27
|
+
export type MiddlewareCallback = (
|
|
28
|
+
request: NextRequest,
|
|
29
|
+
context: MiddlewareContext
|
|
30
|
+
) => Promise<NextResponse | void> | NextResponse | void;
|
|
31
|
+
|
|
10
32
|
/**
|
|
11
33
|
* Legacy Next-intl based middleware
|
|
12
34
|
*/
|
|
@@ -31,13 +53,40 @@ export const createI18nMiddleware = (config: I18nConfig) => {
|
|
|
31
53
|
export const createI18nProxy = createI18nMiddleware;
|
|
32
54
|
|
|
33
55
|
/**
|
|
34
|
-
* Modern composable middleware for Better i18n
|
|
56
|
+
* Modern composable middleware for Better i18n (Clerk-style pattern)
|
|
35
57
|
*
|
|
36
58
|
* Delegates to next-intl's middleware internally to ensure full compatibility
|
|
37
59
|
* with `getRequestConfig({ requestLocale })` while keeping our compose-friendly
|
|
38
60
|
* API and custom locale detection options.
|
|
61
|
+
*
|
|
62
|
+
* @example Simple usage (no callback)
|
|
63
|
+
* ```ts
|
|
64
|
+
* export default createBetterI18nMiddleware({
|
|
65
|
+
* project: "acme/dashboard",
|
|
66
|
+
* defaultLocale: "en",
|
|
67
|
+
* localePrefix: "always",
|
|
68
|
+
* });
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* @example With callback (Clerk-style - recommended for auth)
|
|
72
|
+
* ```ts
|
|
73
|
+
* export default createBetterI18nMiddleware({
|
|
74
|
+
* project: "acme/dashboard",
|
|
75
|
+
* defaultLocale: "en",
|
|
76
|
+
* localePrefix: "always",
|
|
77
|
+
* }, async (request, { locale, response }) => {
|
|
78
|
+
* // Auth logic here - locale and response are available
|
|
79
|
+
* if (needsLogin) {
|
|
80
|
+
* return NextResponse.redirect(new URL(`/${locale}/login`, request.url));
|
|
81
|
+
* }
|
|
82
|
+
* // Return nothing = i18n response is used (headers preserved!)
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
39
85
|
*/
|
|
40
|
-
export function createBetterI18nMiddleware(
|
|
86
|
+
export function createBetterI18nMiddleware(
|
|
87
|
+
config: I18nMiddlewareConfig,
|
|
88
|
+
callback?: MiddlewareCallback
|
|
89
|
+
) {
|
|
41
90
|
const { project, defaultLocale, localePrefix = "as-needed", detection = {} } = config;
|
|
42
91
|
|
|
43
92
|
const {
|
|
@@ -88,40 +137,95 @@ export function createBetterI18nMiddleware(config: I18nMiddlewareConfig) {
|
|
|
88
137
|
// 4. Call next-intl middleware (sets x-middleware-request-x-next-intl-locale header)
|
|
89
138
|
const response = cachedMiddleware(request);
|
|
90
139
|
|
|
91
|
-
// 5.
|
|
92
|
-
|
|
140
|
+
// 5. Get detected locale from next-intl header (more accurate than our detection)
|
|
141
|
+
const detectedLocale =
|
|
142
|
+
response.headers.get("x-middleware-request-x-next-intl-locale") ||
|
|
143
|
+
result.locale;
|
|
93
144
|
|
|
94
|
-
// 6.
|
|
145
|
+
// 6. Add x-locale header for backwards compatibility
|
|
146
|
+
response.headers.set("x-locale", detectedLocale);
|
|
147
|
+
|
|
148
|
+
// 7. Set our custom cookie if needed
|
|
95
149
|
if (cookie && result.shouldSetCookie) {
|
|
96
|
-
response.cookies.set(cookieName,
|
|
150
|
+
response.cookies.set(cookieName, detectedLocale, {
|
|
97
151
|
path: "/",
|
|
98
152
|
maxAge: cookieMaxAge,
|
|
99
153
|
sameSite: "lax",
|
|
100
154
|
});
|
|
101
155
|
}
|
|
102
156
|
|
|
157
|
+
// 8. If callback provided, execute it (Clerk-style)
|
|
158
|
+
if (callback) {
|
|
159
|
+
const callbackResult = await callback(request, {
|
|
160
|
+
locale: detectedLocale,
|
|
161
|
+
response,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// If callback returns a response (e.g., redirect), use it
|
|
165
|
+
if (callbackResult) {
|
|
166
|
+
return callbackResult;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 9. Return the i18n response (with all headers preserved)
|
|
103
171
|
return response;
|
|
104
172
|
};
|
|
105
173
|
}
|
|
106
174
|
|
|
107
175
|
/**
|
|
108
176
|
* Helper to compose multiple Next.js middleware
|
|
177
|
+
*
|
|
178
|
+
* @deprecated Use `createBetterI18nMiddleware` with a callback instead (Clerk-style pattern).
|
|
179
|
+
* The callback approach is more reliable and gives you access to the detected locale.
|
|
180
|
+
*
|
|
181
|
+
* @example Migration
|
|
182
|
+
* ```ts
|
|
183
|
+
* // Before (deprecated):
|
|
184
|
+
* export default composeMiddleware(i18nMiddleware, authMiddleware);
|
|
185
|
+
*
|
|
186
|
+
* // After (recommended):
|
|
187
|
+
* export default createBetterI18nMiddleware(config, async (req, { locale, response }) => {
|
|
188
|
+
* // Auth logic here
|
|
189
|
+
* });
|
|
190
|
+
* ```
|
|
191
|
+
*
|
|
192
|
+
* @see https://github.com/better-i18n/better-i18n#middleware-composition
|
|
109
193
|
*/
|
|
110
194
|
export function composeMiddleware(
|
|
111
195
|
...middlewares: Array<(req: NextRequest) => Promise<NextResponse>>
|
|
112
196
|
) {
|
|
197
|
+
if (process.env.NODE_ENV === "development") {
|
|
198
|
+
console.warn(
|
|
199
|
+
"[@better-i18n/next] composeMiddleware is deprecated. " +
|
|
200
|
+
"Use createBetterI18nMiddleware with a callback instead. " +
|
|
201
|
+
"See: https://github.com/better-i18n/better-i18n#middleware-composition"
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
113
205
|
return async (request: NextRequest): Promise<NextResponse> => {
|
|
114
|
-
|
|
206
|
+
const finalResponse = NextResponse.next();
|
|
115
207
|
|
|
116
208
|
for (const middleware of middlewares) {
|
|
117
|
-
response = await middleware(request);
|
|
209
|
+
const response = await middleware(request);
|
|
118
210
|
|
|
119
211
|
// Short-circuit on redirect/rewrite (status >= 300)
|
|
120
212
|
if (response.status >= 300) {
|
|
121
|
-
|
|
213
|
+
return response;
|
|
122
214
|
}
|
|
215
|
+
|
|
216
|
+
// Merge headers from this middleware into the final response
|
|
217
|
+
response.headers.forEach((value, key) => {
|
|
218
|
+
finalResponse.headers.set(key, value);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Merge cookies from this middleware into the final response
|
|
222
|
+
response.cookies.getAll().forEach((cookie) => {
|
|
223
|
+
finalResponse.cookies.set(cookie.name, cookie.value, {
|
|
224
|
+
...cookie,
|
|
225
|
+
});
|
|
226
|
+
});
|
|
123
227
|
}
|
|
124
228
|
|
|
125
|
-
return
|
|
229
|
+
return finalResponse;
|
|
126
230
|
};
|
|
127
231
|
}
|