@better-i18n/next 0.5.2 → 0.5.4

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/src/middleware.ts DELETED
@@ -1,243 +0,0 @@
1
- import createMiddleware from "next-intl/middleware";
2
- import { NextRequest, NextResponse } from "next/server";
3
- import { createI18nCore, createLogger, detectLocale } from "@better-i18n/core";
4
- import type { I18nMiddlewareConfig } from "@better-i18n/core";
5
-
6
- import { normalizeConfig } from "./config";
7
- import { getLocales } from "./server";
8
- import type { I18nConfig } from "./types";
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
-
32
- /**
33
- * Legacy Next-intl based middleware
34
- */
35
- export const createI18nMiddleware = (config: I18nConfig) => {
36
- const normalized = normalizeConfig(config);
37
- const logger = createLogger(normalized, "middleware");
38
-
39
- return async function middleware(request: NextRequest) {
40
- const locales = await getLocales(normalized);
41
- logger.debug("locales", locales);
42
-
43
- const handleI18nRouting = createMiddleware({
44
- locales,
45
- defaultLocale: normalized.defaultLocale,
46
- localePrefix: normalized.localePrefix,
47
- });
48
-
49
- return handleI18nRouting(request);
50
- };
51
- };
52
-
53
- export const createI18nProxy = createI18nMiddleware;
54
-
55
- /**
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
- * ```
85
- */
86
- export function createBetterI18nMiddleware(
87
- config: I18nMiddlewareConfig,
88
- callback?: MiddlewareCallback
89
- ) {
90
- const { project, defaultLocale, localePrefix = "as-needed", detection = {} } = config;
91
-
92
- const {
93
- cookie = true,
94
- browserLanguage = true,
95
- cookieName = "locale",
96
- cookieMaxAge = 31536000,
97
- } = detection;
98
-
99
- // Create i18n core instance for CDN operations
100
- const i18nCore = createI18nCore({ project, defaultLocale });
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
-
106
- return async (request: NextRequest): Promise<NextResponse> => {
107
- // 1. Fetch available locales from CDN
108
- const availableLocales = await i18nCore.getLocales();
109
-
110
- // 2. Our custom locale detection
111
- // When localePrefix is "never", path has no locale segment — skip it
112
- const pathLocale =
113
- localePrefix === "never"
114
- ? null
115
- : request.nextUrl.pathname.split("/")[1];
116
- const cookieLocale = cookie ? request.cookies.get(cookieName)?.value : null;
117
- const headerLocale = browserLanguage
118
- ? request.headers.get("accept-language")?.split(",")[0]?.split("-")[0]
119
- : null;
120
-
121
- const result = detectLocale({
122
- project,
123
- defaultLocale,
124
- pathLocale,
125
- cookieLocale: cookieLocale || null,
126
- headerLocale,
127
- availableLocales,
128
- });
129
-
130
- const detectedLocale = result.locale;
131
-
132
- let response: NextResponse;
133
-
134
- if (localePrefix === "never") {
135
- // 3a. "never" mode: bypass next-intl middleware entirely.
136
- // next-intl's createMiddleware tries URL rewriting which breaks cookie-only locale.
137
- // We create a plain response and set the header that next-intl's getRequestConfig reads.
138
- response = NextResponse.next();
139
- response.headers.set(
140
- "x-middleware-request-x-next-intl-locale",
141
- detectedLocale
142
- );
143
- } else {
144
- // 3b. URL-based modes: delegate to next-intl middleware as before
145
- const localesKey = availableLocales.join(",");
146
- if (!cachedMiddleware || cachedLocalesKey !== localesKey) {
147
- cachedMiddleware = createMiddleware({
148
- locales: availableLocales,
149
- defaultLocale,
150
- localePrefix,
151
- });
152
- cachedLocalesKey = localesKey;
153
- }
154
- response = cachedMiddleware(request);
155
- }
156
-
157
- // 4. Add x-locale header for backwards compatibility
158
- response.headers.set("x-locale", detectedLocale);
159
-
160
- // 5. Set our custom cookie if needed
161
- if (cookie && result.shouldSetCookie) {
162
- response.cookies.set(cookieName, detectedLocale, {
163
- path: "/",
164
- maxAge: cookieMaxAge,
165
- sameSite: "lax",
166
- });
167
- }
168
-
169
- // 6. If callback provided, execute it (Clerk-style)
170
- if (callback) {
171
- const callbackResult = await callback(request, {
172
- locale: detectedLocale,
173
- response,
174
- });
175
-
176
- // If callback returns a response (e.g., redirect), use it
177
- if (callbackResult) {
178
- return callbackResult;
179
- }
180
- }
181
-
182
- // 7. Return the response
183
- return response;
184
- };
185
- }
186
-
187
- /**
188
- * Helper to compose multiple Next.js middleware
189
- *
190
- * @deprecated Use `createBetterI18nMiddleware` with a callback instead (Clerk-style pattern).
191
- * The callback approach is more reliable and gives you access to the detected locale.
192
- *
193
- * @example Migration
194
- * ```ts
195
- * // Before (deprecated):
196
- * export default composeMiddleware(i18nMiddleware, authMiddleware);
197
- *
198
- * // After (recommended):
199
- * export default createBetterI18nMiddleware(config, async (req, { locale, response }) => {
200
- * // Auth logic here
201
- * });
202
- * ```
203
- *
204
- * @see https://github.com/better-i18n/better-i18n#middleware-composition
205
- */
206
- export function composeMiddleware(
207
- ...middlewares: Array<(req: NextRequest) => Promise<NextResponse>>
208
- ) {
209
- if (process.env.NODE_ENV === "development") {
210
- console.warn(
211
- "[@better-i18n/next] composeMiddleware is deprecated. " +
212
- "Use createBetterI18nMiddleware with a callback instead. " +
213
- "See: https://github.com/better-i18n/better-i18n#middleware-composition"
214
- );
215
- }
216
-
217
- return async (request: NextRequest): Promise<NextResponse> => {
218
- const finalResponse = NextResponse.next();
219
-
220
- for (const middleware of middlewares) {
221
- const response = await middleware(request);
222
-
223
- // Short-circuit on redirect/rewrite (status >= 300)
224
- if (response.status >= 300) {
225
- return response;
226
- }
227
-
228
- // Merge headers from this middleware into the final response
229
- response.headers.forEach((value, key) => {
230
- finalResponse.headers.set(key, value);
231
- });
232
-
233
- // Merge cookies from this middleware into the final response
234
- response.cookies.getAll().forEach((cookie) => {
235
- finalResponse.cookies.set(cookie.name, cookie.value, {
236
- ...cookie,
237
- });
238
- });
239
- }
240
-
241
- return finalResponse;
242
- };
243
- }
package/src/proxy.ts DELETED
@@ -1 +0,0 @@
1
- export { createI18nProxy } from "./middleware";
package/src/server.ts DELETED
@@ -1,164 +0,0 @@
1
- import { getRequestConfig } from "next-intl/server";
2
- import { cookies } from "next/headers";
3
- import { createI18nCore } from "@better-i18n/core";
4
- import type { I18nCore, Messages } from "@better-i18n/core";
5
-
6
- import { normalizeConfig, getProjectBaseUrl } from "./config";
7
- import type { I18nConfig, NextFetchRequestInit } from "./types";
8
-
9
- /**
10
- * Next.js i18n core instance with ISR support
11
- */
12
- export interface NextI18nCore extends I18nCore {
13
- /**
14
- * Get messages for a locale with Next.js ISR revalidation
15
- */
16
- getMessages: (locale: string) => Promise<Messages>;
17
- }
18
-
19
- /**
20
- * Create a fetch function with Next.js ISR revalidation
21
- */
22
- const createIsrFetch = (revalidateSeconds: number): typeof fetch => {
23
- return (input: RequestInfo | URL, init?: RequestInit) => {
24
- const nextInit: NextFetchRequestInit = {
25
- ...init,
26
- next: { revalidate: revalidateSeconds },
27
- };
28
- return fetch(input, nextInit);
29
- };
30
- };
31
-
32
- /**
33
- * Create a Next.js i18n core instance with ISR support
34
- *
35
- * This wraps the framework-agnostic `createI18nCore` from `@better-i18n/core`
36
- * and adds Next.js-specific ISR revalidation for optimal caching.
37
- *
38
- * @example
39
- * ```ts
40
- * const i18n = createNextI18nCore({
41
- * project: 'acme/dashboard',
42
- * defaultLocale: 'en',
43
- * })
44
- *
45
- * // Manifest cached for 1 hour (ISR)
46
- * const locales = await i18n.getLocales()
47
- *
48
- * // Messages revalidated every 30s (ISR)
49
- * const messages = await i18n.getMessages('en')
50
- * ```
51
- */
52
- export const createNextI18nCore = (config: I18nConfig): NextI18nCore => {
53
- const normalized = normalizeConfig(config);
54
-
55
- // Core instance uses ISR fetch for manifest (default 3600s)
56
- const manifestFetch = createIsrFetch(normalized.manifestRevalidateSeconds);
57
- const i18nCore = createI18nCore({
58
- project: normalized.project,
59
- defaultLocale: normalized.defaultLocale,
60
- cdnBaseUrl: normalized.cdnBaseUrl,
61
- manifestCacheTtlMs: normalized.manifestCacheTtlMs,
62
- debug: normalized.debug,
63
- logLevel: normalized.logLevel,
64
- fetch: manifestFetch,
65
- });
66
-
67
- // Messages use separate ISR fetch with shorter revalidation (default 30s)
68
- const messagesFetch = createIsrFetch(normalized.messagesRevalidateSeconds);
69
-
70
- return {
71
- ...i18nCore,
72
- getMessages: async (locale: string): Promise<Messages> => {
73
- const url = `${getProjectBaseUrl(normalized)}/${locale}/translations.json`;
74
- const response = await messagesFetch(url);
75
- if (!response.ok) {
76
- throw new Error(`[better-i18n] Messages fetch failed for locale "${locale}" (${response.status})`);
77
- }
78
- return response.json();
79
- },
80
- };
81
- };
82
-
83
- /**
84
- * Create next-intl request config for App Router
85
- *
86
- * @example
87
- * ```ts
88
- * // i18n/request.ts
89
- * import { createNextIntlRequestConfig } from '@better-i18n/next/server'
90
- *
91
- * export default createNextIntlRequestConfig({
92
- * project: 'acme/dashboard',
93
- * defaultLocale: 'en',
94
- * })
95
- * ```
96
- */
97
- export const createNextIntlRequestConfig = (config: I18nConfig) =>
98
- getRequestConfig(async ({ requestLocale }) => {
99
- const i18n = createNextI18nCore(config);
100
- const normalized = normalizeConfig(config);
101
- const locales = await i18n.getLocales();
102
-
103
- // 1. Middleware header (set by next-intl or our middleware)
104
- let locale = await requestLocale;
105
-
106
- // 2. Cookie fallback — critical for localePrefix: "never" where
107
- // requestLocale may be undefined if middleware header isn't forwarded
108
- if (!locale) {
109
- try {
110
- const cookieStore = await cookies();
111
- const cookieLocale = cookieStore.get(normalized.cookieName)?.value;
112
- if (cookieLocale && locales.includes(cookieLocale)) {
113
- locale = cookieLocale;
114
- }
115
- } catch {
116
- // cookies() throws in non-request contexts (e.g. build time)
117
- }
118
- }
119
-
120
- // 3. Final fallback to defaultLocale
121
- if (!locale || !locales.includes(locale)) {
122
- locale = normalized.defaultLocale;
123
- }
124
-
125
- return {
126
- locale,
127
- messages: await i18n.getMessages(locale),
128
- };
129
- });
130
-
131
- // Convenience exports for backwards compatibility
132
-
133
- /**
134
- * Fetch manifest from CDN
135
- */
136
- export const fetchManifest = (config: I18nConfig) =>
137
- createNextI18nCore(config).getManifest();
138
-
139
- /**
140
- * Get manifest with caching
141
- */
142
- export const getManifest = (config: I18nConfig, options?: { forceRefresh?: boolean }) =>
143
- createNextI18nCore(config).getManifest(options);
144
-
145
- /**
146
- * Get available locale codes
147
- */
148
- export const getLocales = (config: I18nConfig) =>
149
- createNextI18nCore(config).getLocales();
150
-
151
- /**
152
- * Get messages for a locale
153
- */
154
- export const getMessages = (config: I18nConfig, locale: string) =>
155
- createNextI18nCore(config).getMessages(locale);
156
-
157
- /**
158
- * Get language options with metadata
159
- */
160
- export const getManifestLanguages = (config: I18nConfig) =>
161
- createNextI18nCore(config).getLanguages();
162
-
163
- // Re-export types from core
164
- export type { LanguageOption, ManifestLanguage, ManifestResponse, Messages } from "@better-i18n/core";
package/src/types.ts DELETED
@@ -1,66 +0,0 @@
1
- // Re-export common types from i18n-core
2
- export type {
3
- Locale,
4
- LogLevel,
5
- ManifestLanguage,
6
- ManifestFile,
7
- ManifestResponse,
8
- LanguageOption,
9
- Messages,
10
- Logger,
11
- ParsedProject,
12
- } from "@better-i18n/core";
13
-
14
- import type { I18nCoreConfig, NormalizedConfig as CoreNormalizedConfig } from "@better-i18n/core";
15
-
16
- // Next.js-specific types
17
-
18
- export type LocalePrefix = "as-needed" | "always" | "never";
19
-
20
- /**
21
- * Next.js i18n configuration (extends core config)
22
- */
23
- export interface I18nConfig extends I18nCoreConfig {
24
- /**
25
- * URL locale prefix behavior
26
- * @default "as-needed"
27
- */
28
- localePrefix?: LocalePrefix;
29
-
30
- /**
31
- * Cookie name used for locale persistence when localePrefix is "never"
32
- * @default "locale"
33
- */
34
- cookieName?: string;
35
-
36
- /**
37
- * Next.js ISR revalidation time for manifest (seconds)
38
- * @default 3600
39
- */
40
- manifestRevalidateSeconds?: number;
41
-
42
- /**
43
- * Next.js ISR revalidation time for messages (seconds)
44
- * @default 30
45
- */
46
- messagesRevalidateSeconds?: number;
47
- }
48
-
49
- /**
50
- * Normalized Next.js config with defaults
51
- */
52
- export interface NormalizedConfig extends CoreNormalizedConfig, Omit<I18nConfig, keyof I18nCoreConfig> {
53
- localePrefix: LocalePrefix;
54
- cookieName: string;
55
- manifestRevalidateSeconds: number;
56
- messagesRevalidateSeconds: number;
57
- }
58
-
59
- /**
60
- * Next.js fetch request init with ISR options
61
- */
62
- export type NextFetchRequestInit = RequestInit & {
63
- next?: {
64
- revalidate?: number;
65
- };
66
- };