@better-i18n/next 0.2.3 → 0.3.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.3",
3
+ "version": "0.3.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.4"
60
+ "@better-i18n/core": "0.1.6"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "next": ">=15.0.0",
package/src/index.ts CHANGED
@@ -48,6 +48,7 @@ export const createI18n = (config: I18nConfig) => {
48
48
 
49
49
  // Modern standalone middleware exports
50
50
  export { createBetterI18nMiddleware, composeMiddleware };
51
+ export type { MiddlewareContext, MiddlewareCallback } from "./middleware";
51
52
 
52
53
  // Core instance factory
53
54
  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,10 +53,41 @@ 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)
57
+ *
58
+ * Delegates to next-intl's middleware internally to ensure full compatibility
59
+ * with `getRequestConfig({ requestLocale })` while keeping our compose-friendly
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
+ * ```
35
85
  */
36
- export function createBetterI18nMiddleware(config: I18nMiddlewareConfig) {
37
- const { project, defaultLocale, detection = {} } = config;
86
+ export function createBetterI18nMiddleware(
87
+ config: I18nMiddlewareConfig,
88
+ callback?: MiddlewareCallback
89
+ ) {
90
+ const { project, defaultLocale, localePrefix = "as-needed", detection = {} } = config;
38
91
 
39
92
  const {
40
93
  cookie = true,
@@ -46,18 +99,32 @@ export function createBetterI18nMiddleware(config: I18nMiddlewareConfig) {
46
99
  // Create i18n core instance for CDN operations
47
100
  const i18nCore = createI18nCore({ project, defaultLocale });
48
101
 
102
+ // Cache for next-intl middleware instance (recreate only when locales change)
103
+ let cachedMiddleware: ReturnType<typeof createMiddleware> | null = null;
104
+ let cachedLocalesKey: string | null = null;
105
+
49
106
  return async (request: NextRequest): Promise<NextResponse> => {
50
107
  // 1. Fetch available locales from CDN
51
108
  const availableLocales = await i18nCore.getLocales();
52
109
 
53
- // 2. Extract locale indicators
110
+ // 2. Create/reuse next-intl middleware (only recreate if locales changed)
111
+ const localesKey = availableLocales.join(",");
112
+ if (!cachedMiddleware || cachedLocalesKey !== localesKey) {
113
+ cachedMiddleware = createMiddleware({
114
+ locales: availableLocales,
115
+ defaultLocale,
116
+ localePrefix,
117
+ });
118
+ cachedLocalesKey = localesKey;
119
+ }
120
+
121
+ // 3. Our custom locale detection (for cookie logic)
54
122
  const pathLocale = request.nextUrl.pathname.split("/")[1];
55
123
  const cookieLocale = cookie ? request.cookies.get(cookieName)?.value : null;
56
124
  const headerLocale = browserLanguage
57
125
  ? request.headers.get("accept-language")?.split(",")[0]?.split("-")[0]
58
126
  : null;
59
127
 
60
- // 3. Detect locale using core logic
61
128
  const result = detectLocale({
62
129
  project,
63
130
  defaultLocale,
@@ -67,44 +134,98 @@ export function createBetterI18nMiddleware(config: I18nMiddlewareConfig) {
67
134
  availableLocales,
68
135
  });
69
136
 
70
- // 4. Create response with locale header for Server Components
71
- const response = NextResponse.next({
72
- headers: {
73
- "x-locale": result.locale,
74
- },
75
- });
137
+ // 4. Call next-intl middleware (sets x-middleware-request-x-next-intl-locale header)
138
+ const response = cachedMiddleware(request);
76
139
 
77
- // 5. Set cookie if needed
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;
144
+
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
78
149
  if (cookie && result.shouldSetCookie) {
79
- response.cookies.set(cookieName, result.locale, {
150
+ response.cookies.set(cookieName, detectedLocale, {
80
151
  path: "/",
81
152
  maxAge: cookieMaxAge,
82
153
  sameSite: "lax",
83
154
  });
84
155
  }
85
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)
86
171
  return response;
87
172
  };
88
173
  }
89
174
 
90
175
  /**
91
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
92
193
  */
93
194
  export function composeMiddleware(
94
195
  ...middlewares: Array<(req: NextRequest) => Promise<NextResponse>>
95
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
+
96
205
  return async (request: NextRequest): Promise<NextResponse> => {
97
- let response = NextResponse.next();
206
+ const finalResponse = NextResponse.next();
98
207
 
99
208
  for (const middleware of middlewares) {
100
- response = await middleware(request);
209
+ const response = await middleware(request);
101
210
 
102
211
  // Short-circuit on redirect/rewrite (status >= 300)
103
212
  if (response.status >= 300) {
104
- break;
213
+ return response;
105
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
+ });
106
227
  }
107
228
 
108
- return response;
229
+ return finalResponse;
109
230
  };
110
231
  }