@better-i18n/next 0.1.0 → 0.1.3
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/README.md +31 -28
- package/package.json +9 -2
- package/src/client.ts +2 -3
- package/src/config.ts +27 -15
- package/src/core.ts +2 -2
- package/src/logger.ts +2 -1
- package/src/manifest.ts +2 -28
- package/src/middleware.ts +80 -1
- package/src/proxy.ts +1 -0
- package/src/types.ts +43 -54
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @better-i18n/next
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Next.js integration for [Better i18n](https://better-i18n.com). Handles `next-intl` wiring, middleware, and CDN-based translation fetching.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,20 +8,23 @@ Headless Next.js integration for Better i18n. This package owns your `next-intl`
|
|
|
8
8
|
npm i @better-i18n/next next-intl
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
## Quick
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### 1. Create i18n config
|
|
12
14
|
|
|
13
15
|
```ts
|
|
14
16
|
// i18n.ts
|
|
15
17
|
import { createI18n } from "@better-i18n/next";
|
|
16
18
|
|
|
17
19
|
export const i18n = createI18n({
|
|
18
|
-
|
|
19
|
-
projectSlug: "landing",
|
|
20
|
+
project: "your-org/your-project", // Format: "org-slug/project-slug"
|
|
20
21
|
defaultLocale: "en",
|
|
21
22
|
debug: process.env.NODE_ENV !== "production",
|
|
22
23
|
});
|
|
23
24
|
```
|
|
24
25
|
|
|
26
|
+
### 2. Setup request config
|
|
27
|
+
|
|
25
28
|
```ts
|
|
26
29
|
// app/i18n/request.ts
|
|
27
30
|
import { i18n } from "./i18n";
|
|
@@ -29,25 +32,20 @@ import { i18n } from "./i18n";
|
|
|
29
32
|
export default i18n.requestConfig;
|
|
30
33
|
```
|
|
31
34
|
|
|
35
|
+
### 3. Add middleware
|
|
36
|
+
|
|
32
37
|
```ts
|
|
33
|
-
// middleware.ts
|
|
38
|
+
// middleware.ts
|
|
34
39
|
import { i18n } from "./i18n";
|
|
35
40
|
|
|
36
41
|
export default i18n.middleware;
|
|
37
42
|
|
|
38
43
|
export const config = {
|
|
39
|
-
matcher: ["/", "/((?!api|_next|_vercel|.*\\..*).*)"],
|
|
44
|
+
matcher: ["/", "/((?!api|_next|_vercel|.*\\..*).*)" ],
|
|
40
45
|
};
|
|
41
46
|
```
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
// proxy.ts (Next 16+)
|
|
45
|
-
import { i18n } from "./i18n";
|
|
46
|
-
|
|
47
|
-
export default i18n.proxy;
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## Client hook
|
|
48
|
+
## Client Hook
|
|
51
49
|
|
|
52
50
|
```tsx
|
|
53
51
|
"use client";
|
|
@@ -56,8 +54,7 @@ import { useManifestLanguages } from "@better-i18n/next/client";
|
|
|
56
54
|
|
|
57
55
|
export function LanguageSwitcher() {
|
|
58
56
|
const { languages, isLoading, error } = useManifestLanguages({
|
|
59
|
-
|
|
60
|
-
projectSlug: "landing",
|
|
57
|
+
project: "your-org/your-project",
|
|
61
58
|
defaultLocale: "en",
|
|
62
59
|
});
|
|
63
60
|
|
|
@@ -74,27 +71,33 @@ export function LanguageSwitcher() {
|
|
|
74
71
|
}
|
|
75
72
|
```
|
|
76
73
|
|
|
77
|
-
## Server
|
|
74
|
+
## Server Helpers
|
|
78
75
|
|
|
79
76
|
```ts
|
|
80
77
|
import { getLocales, getMessages } from "@better-i18n/next/server";
|
|
81
78
|
|
|
82
|
-
const
|
|
83
|
-
|
|
79
|
+
const config = { project: "your-org/your-project", defaultLocale: "en" };
|
|
80
|
+
|
|
81
|
+
const locales = await getLocales(config);
|
|
82
|
+
const messages = await getMessages(config, "tr");
|
|
84
83
|
```
|
|
85
84
|
|
|
86
|
-
##
|
|
85
|
+
## Configuration Options
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
| Option | Type | Description |
|
|
88
|
+
|--------|------|-------------|
|
|
89
|
+
| `project` | `string` | **Required.** Format: `"org-slug/project-slug"` |
|
|
90
|
+
| `defaultLocale` | `string` | **Required.** Default locale code (e.g., `"en"`) |
|
|
91
|
+
| `cdnBaseUrl` | `string` | CDN URL (default: `https://cdn.better-i18n.com`) |
|
|
92
|
+
| `debug` | `boolean` | Enable verbose logging |
|
|
93
|
+
| `localePrefix` | `"as-needed" \| "always" \| "never"` | URL prefix strategy |
|
|
90
94
|
|
|
91
|
-
|
|
95
|
+
## Debug Logging
|
|
92
96
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
-
|
|
96
|
-
- For Next apps, ensure `transpilePackages: ["@better-i18n/next"]` if your bundler doesn’t compile TS in node_modules.
|
|
97
|
+
Enable with environment variables:
|
|
98
|
+
- `BETTER_I18N_DEBUG=1`
|
|
99
|
+
- `BETTER_I18N_LOG_LEVEL=debug|info|warn|error|silent`
|
|
97
100
|
|
|
98
101
|
## License
|
|
99
102
|
|
|
100
|
-
MIT
|
|
103
|
+
MIT © [Better i18n](https://better-i18n.com)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-i18n/next",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Better-i18n Next.js integration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
"types": "./src/middleware.ts",
|
|
33
33
|
"default": "./src/middleware.ts"
|
|
34
34
|
},
|
|
35
|
+
"./proxy": {
|
|
36
|
+
"types": "./src/proxy.ts",
|
|
37
|
+
"default": "./src/proxy.ts"
|
|
38
|
+
},
|
|
35
39
|
"./package.json": "./package.json"
|
|
36
40
|
},
|
|
37
41
|
"files": [
|
|
@@ -52,6 +56,9 @@
|
|
|
52
56
|
"scripts": {
|
|
53
57
|
"typecheck": "tsc --noEmit"
|
|
54
58
|
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@better-i18n/core": "workspace:*"
|
|
61
|
+
},
|
|
55
62
|
"peerDependencies": {
|
|
56
63
|
"next": ">=15.0.0",
|
|
57
64
|
"next-intl": ">=4.0.0",
|
|
@@ -61,7 +68,7 @@
|
|
|
61
68
|
"devDependencies": {
|
|
62
69
|
"next": "^15.4.4",
|
|
63
70
|
"next-intl": "^4.5.8",
|
|
64
|
-
"@
|
|
71
|
+
"@better-i18n/typescript-config": "workspace:*",
|
|
65
72
|
"@types/node": "^20.0.0",
|
|
66
73
|
"@types/react": "^19.0.0",
|
|
67
74
|
"typescript": "~5.9.2"
|
package/src/client.ts
CHANGED
|
@@ -26,7 +26,7 @@ type ClientCacheEntry = {
|
|
|
26
26
|
const clientCache = new Map<string, ClientCacheEntry>();
|
|
27
27
|
|
|
28
28
|
const getCacheKey = (config: I18nConfig) =>
|
|
29
|
-
`${config.cdnBaseUrl || "https://cdn.better-i18n.com"}|${config.
|
|
29
|
+
`${config.cdnBaseUrl || "https://cdn.better-i18n.com"}|${config.project}`;
|
|
30
30
|
|
|
31
31
|
const fetchManifestLanguages = async (
|
|
32
32
|
config: I18nConfig,
|
|
@@ -62,8 +62,7 @@ export const useManifestLanguages = (
|
|
|
62
62
|
const normalized = useMemo(
|
|
63
63
|
() => normalizeConfig(config),
|
|
64
64
|
[
|
|
65
|
-
config.
|
|
66
|
-
config.projectSlug,
|
|
65
|
+
config.project,
|
|
67
66
|
config.defaultLocale,
|
|
68
67
|
config.cdnBaseUrl,
|
|
69
68
|
config.localePrefix,
|
package/src/config.ts
CHANGED
|
@@ -1,27 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseProject,
|
|
3
|
+
getProjectBaseUrl as coreGetProjectBaseUrl,
|
|
4
|
+
normalizeConfig as coreNormalizeConfig,
|
|
5
|
+
} from "@better-i18n/core";
|
|
1
6
|
import type { I18nConfig, NormalizedConfig } from "./types";
|
|
2
7
|
|
|
3
|
-
|
|
8
|
+
// Re-export parseProject from core
|
|
9
|
+
export { parseProject };
|
|
4
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Normalize Next.js i18n config with defaults
|
|
13
|
+
*/
|
|
5
14
|
export const normalizeConfig = (config: I18nConfig): NormalizedConfig => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
// Use core normalization for base config
|
|
16
|
+
const coreConfig = coreNormalizeConfig({
|
|
17
|
+
project: config.project,
|
|
18
|
+
defaultLocale: config.defaultLocale,
|
|
19
|
+
cdnBaseUrl: config.cdnBaseUrl,
|
|
20
|
+
manifestCacheTtlMs: config.manifestCacheTtlMs,
|
|
21
|
+
debug: config.debug,
|
|
22
|
+
logLevel: config.logLevel,
|
|
23
|
+
fetch: config.fetch,
|
|
24
|
+
});
|
|
15
25
|
|
|
26
|
+
// Add Next.js-specific defaults
|
|
16
27
|
return {
|
|
17
|
-
...
|
|
18
|
-
cdnBaseUrl: config.cdnBaseUrl?.replace(/\/$/, "") || DEFAULT_CDN_BASE_URL,
|
|
28
|
+
...coreConfig,
|
|
19
29
|
localePrefix: config.localePrefix ?? "as-needed",
|
|
20
|
-
manifestCacheTtlMs: config.manifestCacheTtlMs ?? 5 * 60 * 1000,
|
|
21
30
|
manifestRevalidateSeconds: config.manifestRevalidateSeconds ?? 3600,
|
|
22
31
|
messagesRevalidateSeconds: config.messagesRevalidateSeconds ?? 30,
|
|
23
32
|
};
|
|
24
33
|
};
|
|
25
34
|
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Get the project base URL on CDN
|
|
37
|
+
*/
|
|
38
|
+
export const getProjectBaseUrl = (config: NormalizedConfig): string =>
|
|
39
|
+
coreGetProjectBaseUrl(config);
|
package/src/core.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getProjectBaseUrl, normalizeConfig } from "./config";
|
|
2
2
|
import { createLogger } from "./logger";
|
|
3
3
|
import { extractLanguages } from "./manifest";
|
|
4
4
|
import type {
|
|
@@ -18,7 +18,7 @@ type ManifestCacheEntry = {
|
|
|
18
18
|
const manifestCache = new Map<string, ManifestCacheEntry>();
|
|
19
19
|
|
|
20
20
|
const buildCacheKey = (config: NormalizedConfig) =>
|
|
21
|
-
`${config.cdnBaseUrl}|${config.
|
|
21
|
+
`${config.cdnBaseUrl}|${config.project}`;
|
|
22
22
|
|
|
23
23
|
const withRevalidate = (
|
|
24
24
|
init: NextFetchRequestInit,
|
package/src/logger.ts
CHANGED
package/src/manifest.ts
CHANGED
|
@@ -1,28 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
ManifestLanguage,
|
|
4
|
-
ManifestResponse,
|
|
5
|
-
} from "./types";
|
|
6
|
-
|
|
7
|
-
const normalizeLanguage = (language: ManifestLanguage): LanguageOption => ({
|
|
8
|
-
code: language.code,
|
|
9
|
-
name: language.name,
|
|
10
|
-
nativeName:
|
|
11
|
-
language.nativeName || language.name || language.code.toUpperCase(),
|
|
12
|
-
flagUrl: language.flagUrl ?? null,
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
export const extractLanguages = (
|
|
16
|
-
manifest: ManifestResponse,
|
|
17
|
-
): LanguageOption[] => {
|
|
18
|
-
const languages = Array.isArray(manifest.languages)
|
|
19
|
-
? manifest.languages
|
|
20
|
-
: [];
|
|
21
|
-
|
|
22
|
-
return languages
|
|
23
|
-
.filter(
|
|
24
|
-
(language): language is ManifestLanguage =>
|
|
25
|
-
!!language && typeof language.code === "string" && language.code.length > 0,
|
|
26
|
-
)
|
|
27
|
-
.map(normalizeLanguage);
|
|
28
|
-
};
|
|
1
|
+
// Re-export from i18n-core
|
|
2
|
+
export { extractLanguages } from "@better-i18n/core";
|
package/src/middleware.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import createMiddleware from "next-intl/middleware";
|
|
2
|
-
import
|
|
2
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
3
|
+
// @ts-ignore - internal workspace dependency
|
|
4
|
+
import { detectLocale, getLocales as getCDNLocales } from "@better-i18n/core";
|
|
5
|
+
// @ts-ignore - internal workspace dependency
|
|
6
|
+
import type { I18nMiddlewareConfig } from "@better-i18n/core";
|
|
3
7
|
|
|
4
8
|
import { normalizeConfig } from "./config";
|
|
5
9
|
import { createLogger } from "./logger";
|
|
6
10
|
import { getLocales } from "./core";
|
|
7
11
|
import type { I18nConfig } from "./types";
|
|
8
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Legacy Next-intl based middleware
|
|
15
|
+
*/
|
|
9
16
|
export const createI18nMiddleware = (config: I18nConfig) => {
|
|
10
17
|
const normalized = normalizeConfig(config);
|
|
11
18
|
const logger = createLogger(normalized, "middleware");
|
|
@@ -25,3 +32,75 @@ export const createI18nMiddleware = (config: I18nConfig) => {
|
|
|
25
32
|
};
|
|
26
33
|
|
|
27
34
|
export const createI18nProxy = createI18nMiddleware;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Modern composable middleware for Better i18n
|
|
38
|
+
*/
|
|
39
|
+
export function createBetterI18nMiddleware(config: I18nMiddlewareConfig) {
|
|
40
|
+
const { project, defaultLocale, detection = {} } = config;
|
|
41
|
+
|
|
42
|
+
const {
|
|
43
|
+
cookie = true,
|
|
44
|
+
browserLanguage = true,
|
|
45
|
+
cookieName = "locale",
|
|
46
|
+
cookieMaxAge = 31536000,
|
|
47
|
+
} = detection;
|
|
48
|
+
|
|
49
|
+
return async (request: NextRequest): Promise<NextResponse> => {
|
|
50
|
+
// 1. Fetch available locales from CDN
|
|
51
|
+
const availableLocales = await getCDNLocales({ project });
|
|
52
|
+
|
|
53
|
+
// 2. Extract locale indicators
|
|
54
|
+
const pathLocale = request.nextUrl.pathname.split("/")[1];
|
|
55
|
+
const cookieLocale = cookie ? request.cookies.get(cookieName)?.value : null;
|
|
56
|
+
const headerLocale = browserLanguage
|
|
57
|
+
? request.headers.get("accept-language")?.split(",")[0]?.split("-")[0]
|
|
58
|
+
: null;
|
|
59
|
+
|
|
60
|
+
// 3. Detect locale using core logic
|
|
61
|
+
const result = detectLocale({
|
|
62
|
+
project,
|
|
63
|
+
defaultLocale,
|
|
64
|
+
pathLocale,
|
|
65
|
+
cookieLocale: cookieLocale || null,
|
|
66
|
+
headerLocale,
|
|
67
|
+
availableLocales,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// 4. Create response
|
|
71
|
+
const response = NextResponse.next();
|
|
72
|
+
|
|
73
|
+
// 5. Set cookie if needed
|
|
74
|
+
if (cookie && result.shouldSetCookie) {
|
|
75
|
+
response.cookies.set(cookieName, result.locale, {
|
|
76
|
+
path: "/",
|
|
77
|
+
maxAge: cookieMaxAge,
|
|
78
|
+
sameSite: "lax",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return response;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Helper to compose multiple Next.js middleware
|
|
88
|
+
*/
|
|
89
|
+
export function composeMiddleware(
|
|
90
|
+
...middlewares: Array<(req: NextRequest) => Promise<NextResponse>>
|
|
91
|
+
) {
|
|
92
|
+
return async (request: NextRequest): Promise<NextResponse> => {
|
|
93
|
+
let response = NextResponse.next();
|
|
94
|
+
|
|
95
|
+
for (const middleware of middlewares) {
|
|
96
|
+
response = await middleware(request);
|
|
97
|
+
|
|
98
|
+
// Short-circuit on redirect/rewrite (status >= 300)
|
|
99
|
+
if (response.status >= 300) {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return response;
|
|
105
|
+
};
|
|
106
|
+
}
|
package/src/proxy.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createI18nProxy } from "./middleware";
|
package/src/types.ts
CHANGED
|
@@ -1,68 +1,57 @@
|
|
|
1
|
-
export
|
|
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
|
|
2
17
|
|
|
3
18
|
export type LocalePrefix = "as-needed" | "always" | "never";
|
|
4
19
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
+
*/
|
|
12
28
|
localePrefix?: LocalePrefix;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Next.js ISR revalidation time for manifest (seconds)
|
|
32
|
+
* @default 3600
|
|
33
|
+
*/
|
|
16
34
|
manifestRevalidateSeconds?: number;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Next.js ISR revalidation time for messages (seconds)
|
|
38
|
+
* @default 30
|
|
39
|
+
*/
|
|
17
40
|
messagesRevalidateSeconds?: number;
|
|
18
|
-
fetch?: typeof fetch;
|
|
19
41
|
}
|
|
20
42
|
|
|
21
|
-
|
|
22
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Normalized Next.js config with defaults
|
|
45
|
+
*/
|
|
46
|
+
export interface NormalizedConfig extends CoreNormalizedConfig, Omit<I18nConfig, keyof I18nCoreConfig> {
|
|
23
47
|
localePrefix: LocalePrefix;
|
|
48
|
+
manifestRevalidateSeconds: number;
|
|
49
|
+
messagesRevalidateSeconds: number;
|
|
24
50
|
}
|
|
25
51
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
nativeName?: string;
|
|
30
|
-
flagUrl?: string | null;
|
|
31
|
-
isSource?: boolean;
|
|
32
|
-
lastUpdated?: string | null;
|
|
33
|
-
keyCount?: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface ManifestFile {
|
|
37
|
-
url: string;
|
|
38
|
-
size: number;
|
|
39
|
-
lastModified: string | null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface ManifestResponse {
|
|
43
|
-
projectSlug?: string;
|
|
44
|
-
sourceLanguage?: string;
|
|
45
|
-
languages: ManifestLanguage[];
|
|
46
|
-
files?: Record<string, ManifestFile>;
|
|
47
|
-
updatedAt?: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface LanguageOption {
|
|
51
|
-
code: string;
|
|
52
|
-
name?: string;
|
|
53
|
-
nativeName?: string;
|
|
54
|
-
flagUrl?: string | null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export type Messages = Record<string, any>;
|
|
58
|
-
|
|
59
|
-
export interface Logger {
|
|
60
|
-
debug: (...args: unknown[]) => void;
|
|
61
|
-
info: (...args: unknown[]) => void;
|
|
62
|
-
warn: (...args: unknown[]) => void;
|
|
63
|
-
error: (...args: unknown[]) => void;
|
|
64
|
-
}
|
|
65
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Next.js fetch request init with ISR options
|
|
54
|
+
*/
|
|
66
55
|
export type NextFetchRequestInit = RequestInit & {
|
|
67
56
|
next?: {
|
|
68
57
|
revalidate?: number;
|