@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-i18n/next",
3
- "version": "0.2.4",
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.5"
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(config: I18nMiddlewareConfig) {
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. Add x-locale header for backwards compatibility
92
- response.headers.set("x-locale", result.locale);
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. Set our custom cookie if needed
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, result.locale, {
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
- let response = NextResponse.next();
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
- break;
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 response;
229
+ return finalResponse;
126
230
  };
127
231
  }