@better-i18n/next 0.1.1 → 0.2.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/README.md +31 -28
- package/package.json +5 -2
- package/src/client.ts +80 -90
- package/src/config.ts +26 -31
- package/src/index.ts +43 -8
- package/src/middleware.ts +86 -3
- package/src/server.ts +131 -8
- package/src/types.ts +43 -60
- package/src/core.ts +0 -147
- package/src/logger.ts +0 -67
- package/src/manifest.ts +0 -28
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.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Better-i18n Next.js integration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -56,6 +56,9 @@
|
|
|
56
56
|
"scripts": {
|
|
57
57
|
"typecheck": "tsc --noEmit"
|
|
58
58
|
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@better-i18n/core": "workspace:*"
|
|
61
|
+
},
|
|
59
62
|
"peerDependencies": {
|
|
60
63
|
"next": ">=15.0.0",
|
|
61
64
|
"next-intl": ">=4.0.0",
|
|
@@ -65,7 +68,7 @@
|
|
|
65
68
|
"devDependencies": {
|
|
66
69
|
"next": "^15.4.4",
|
|
67
70
|
"next-intl": "^4.5.8",
|
|
68
|
-
"@
|
|
71
|
+
"@better-i18n/typescript-config": "workspace:*",
|
|
69
72
|
"@types/node": "^20.0.0",
|
|
70
73
|
"@types/react": "^19.0.0",
|
|
71
74
|
"typescript": "~5.9.2"
|
package/src/client.ts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { createI18nCore } from "@better-i18n/core";
|
|
5
|
+
import type { LanguageOption } from "@better-i18n/core";
|
|
4
6
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { extractLanguages } from "./manifest";
|
|
8
|
-
import type {
|
|
9
|
-
I18nConfig,
|
|
10
|
-
LanguageOption,
|
|
11
|
-
ManifestResponse,
|
|
12
|
-
} from "./types";
|
|
7
|
+
import { normalizeConfig } from "./config";
|
|
8
|
+
import type { I18nConfig } from "./types";
|
|
13
9
|
|
|
14
10
|
export type UseManifestLanguagesResult = {
|
|
15
11
|
languages: LanguageOption[];
|
|
@@ -23,116 +19,111 @@ type ClientCacheEntry = {
|
|
|
23
19
|
promise?: Promise<LanguageOption[]>;
|
|
24
20
|
};
|
|
25
21
|
|
|
22
|
+
// Client-side request deduplication cache
|
|
26
23
|
const clientCache = new Map<string, ClientCacheEntry>();
|
|
27
24
|
|
|
28
|
-
const getCacheKey = (
|
|
29
|
-
`${
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
export const useManifestLanguages = (
|
|
60
|
-
config: I18nConfig,
|
|
61
|
-
): UseManifestLanguagesResult => {
|
|
25
|
+
const getCacheKey = (project: string, cdnBaseUrl?: string) =>
|
|
26
|
+
`${cdnBaseUrl || "https://cdn.better-i18n.com"}|${project}`;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* React hook to fetch manifest languages on the client
|
|
30
|
+
*
|
|
31
|
+
* Uses `createI18nCore` from `@better-i18n/core` internally with
|
|
32
|
+
* request deduplication to prevent duplicate fetches.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* const { languages, isLoading, error } = useManifestLanguages({
|
|
37
|
+
* project: 'acme/dashboard',
|
|
38
|
+
* defaultLocale: 'en',
|
|
39
|
+
* })
|
|
40
|
+
*
|
|
41
|
+
* if (isLoading) return <Spinner />
|
|
42
|
+
* if (error) return <Error message={error.message} />
|
|
43
|
+
*
|
|
44
|
+
* return (
|
|
45
|
+
* <select>
|
|
46
|
+
* {languages.map(lang => (
|
|
47
|
+
* <option key={lang.code} value={lang.code}>
|
|
48
|
+
* {lang.nativeName || lang.name || lang.code}
|
|
49
|
+
* </option>
|
|
50
|
+
* ))}
|
|
51
|
+
* </select>
|
|
52
|
+
* )
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export const useManifestLanguages = (config: I18nConfig): UseManifestLanguagesResult => {
|
|
62
56
|
const normalized = useMemo(
|
|
63
57
|
() => normalizeConfig(config),
|
|
64
58
|
[
|
|
65
59
|
config.project,
|
|
66
60
|
config.defaultLocale,
|
|
67
61
|
config.cdnBaseUrl,
|
|
68
|
-
config.localePrefix,
|
|
69
62
|
config.debug,
|
|
70
63
|
config.logLevel,
|
|
71
|
-
config.manifestCacheTtlMs,
|
|
72
|
-
config.manifestRevalidateSeconds,
|
|
73
|
-
config.messagesRevalidateSeconds,
|
|
74
|
-
config.fetch,
|
|
75
64
|
],
|
|
76
65
|
);
|
|
77
|
-
const logger = useMemo(() => createLogger(normalized, "client"), [
|
|
78
|
-
normalized,
|
|
79
|
-
]);
|
|
80
66
|
|
|
81
|
-
const
|
|
67
|
+
const i18nCore = useMemo(
|
|
68
|
+
() =>
|
|
69
|
+
createI18nCore({
|
|
70
|
+
project: normalized.project,
|
|
71
|
+
defaultLocale: normalized.defaultLocale,
|
|
72
|
+
cdnBaseUrl: normalized.cdnBaseUrl,
|
|
73
|
+
debug: normalized.debug,
|
|
74
|
+
logLevel: normalized.logLevel,
|
|
75
|
+
}),
|
|
76
|
+
[normalized],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const cacheKey = getCacheKey(normalized.project, normalized.cdnBaseUrl);
|
|
82
80
|
const cached = clientCache.get(cacheKey);
|
|
83
81
|
|
|
84
|
-
const [languages, setLanguages] = useState<LanguageOption[]>(
|
|
85
|
-
cached?.data ?? [],
|
|
86
|
-
);
|
|
82
|
+
const [languages, setLanguages] = useState<LanguageOption[]>(cached?.data ?? []);
|
|
87
83
|
const [isLoading, setIsLoading] = useState(!cached?.data);
|
|
88
84
|
const [error, setError] = useState<Error | null>(cached?.error ?? null);
|
|
89
85
|
|
|
90
86
|
useEffect(() => {
|
|
91
87
|
let isMounted = true;
|
|
92
|
-
const controller = new AbortController();
|
|
93
88
|
|
|
94
89
|
const run = async () => {
|
|
95
|
-
|
|
96
|
-
const entry = clientCache.get(cacheKey) ?? {};
|
|
97
|
-
|
|
98
|
-
if (entry.data) {
|
|
99
|
-
if (isMounted) {
|
|
100
|
-
setLanguages(entry.data);
|
|
101
|
-
setError(entry.error ?? null);
|
|
102
|
-
setIsLoading(false);
|
|
103
|
-
}
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
90
|
+
const entry = clientCache.get(cacheKey) ?? {};
|
|
106
91
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
)
|
|
113
|
-
.then((nextLanguages) => {
|
|
114
|
-
clientCache.set(cacheKey, { data: nextLanguages });
|
|
115
|
-
return nextLanguages;
|
|
116
|
-
})
|
|
117
|
-
.catch((err) => {
|
|
118
|
-
const nextError = err instanceof Error ? err : new Error(String(err));
|
|
119
|
-
clientCache.set(cacheKey, { error: nextError });
|
|
120
|
-
throw nextError;
|
|
121
|
-
});
|
|
122
|
-
clientCache.set(cacheKey, entry);
|
|
92
|
+
// Return cached data if available
|
|
93
|
+
if (entry.data) {
|
|
94
|
+
if (isMounted) {
|
|
95
|
+
setLanguages(entry.data);
|
|
96
|
+
setIsLoading(false);
|
|
123
97
|
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
124
100
|
|
|
125
|
-
|
|
101
|
+
// Deduplicate in-flight requests
|
|
102
|
+
if (!entry.promise) {
|
|
103
|
+
entry.promise = i18nCore
|
|
104
|
+
.getLanguages()
|
|
105
|
+
.then((langs) => {
|
|
106
|
+
clientCache.set(cacheKey, { data: langs });
|
|
107
|
+
return langs;
|
|
108
|
+
})
|
|
109
|
+
.catch((err) => {
|
|
110
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
111
|
+
clientCache.set(cacheKey, { error });
|
|
112
|
+
throw error;
|
|
113
|
+
});
|
|
114
|
+
clientCache.set(cacheKey, entry);
|
|
115
|
+
}
|
|
126
116
|
|
|
117
|
+
try {
|
|
118
|
+
const langs = await entry.promise;
|
|
127
119
|
if (isMounted) {
|
|
128
|
-
setLanguages(
|
|
120
|
+
setLanguages(langs);
|
|
129
121
|
setError(null);
|
|
130
122
|
}
|
|
131
123
|
} catch (err) {
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
setError(nextError);
|
|
124
|
+
if (isMounted) {
|
|
125
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
126
|
+
}
|
|
136
127
|
} finally {
|
|
137
128
|
if (isMounted) {
|
|
138
129
|
setIsLoading(false);
|
|
@@ -144,9 +135,8 @@ export const useManifestLanguages = (
|
|
|
144
135
|
|
|
145
136
|
return () => {
|
|
146
137
|
isMounted = false;
|
|
147
|
-
controller.abort();
|
|
148
138
|
};
|
|
149
|
-
}, [cacheKey,
|
|
139
|
+
}, [cacheKey, i18nCore]);
|
|
150
140
|
|
|
151
141
|
return { languages, isLoading, error };
|
|
152
142
|
};
|
package/src/config.ts
CHANGED
|
@@ -1,44 +1,39 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
parseProject,
|
|
3
|
+
getProjectBaseUrl as coreGetProjectBaseUrl,
|
|
4
|
+
normalizeConfig as coreNormalizeConfig,
|
|
5
|
+
} from "@better-i18n/core";
|
|
6
|
+
import type { I18nConfig, NormalizedConfig } from "./types";
|
|
2
7
|
|
|
3
|
-
|
|
8
|
+
// Re-export parseProject from core
|
|
9
|
+
export { parseProject };
|
|
4
10
|
|
|
5
11
|
/**
|
|
6
|
-
*
|
|
12
|
+
* Normalize Next.js i18n config with defaults
|
|
7
13
|
*/
|
|
8
|
-
export const parseProject = (project: string): ParsedProject => {
|
|
9
|
-
const parts = project.split("/");
|
|
10
|
-
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
11
|
-
throw new Error(
|
|
12
|
-
`[better-i18n] Invalid project format "${project}". Expected "org/project" (e.g., "carna/landing")`
|
|
13
|
-
);
|
|
14
|
-
}
|
|
15
|
-
return {
|
|
16
|
-
workspaceId: parts[0],
|
|
17
|
-
projectSlug: parts[1],
|
|
18
|
-
};
|
|
19
|
-
};
|
|
20
|
-
|
|
21
14
|
export const normalizeConfig = (config: I18nConfig): NormalizedConfig => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
});
|
|
30
25
|
|
|
26
|
+
// Add Next.js-specific defaults
|
|
31
27
|
return {
|
|
32
|
-
...
|
|
33
|
-
workspaceId,
|
|
34
|
-
projectSlug,
|
|
35
|
-
cdnBaseUrl: config.cdnBaseUrl?.replace(/\/$/, "") || DEFAULT_CDN_BASE_URL,
|
|
28
|
+
...coreConfig,
|
|
36
29
|
localePrefix: config.localePrefix ?? "as-needed",
|
|
37
|
-
manifestCacheTtlMs: config.manifestCacheTtlMs ?? 5 * 60 * 1000,
|
|
38
30
|
manifestRevalidateSeconds: config.manifestRevalidateSeconds ?? 3600,
|
|
39
31
|
messagesRevalidateSeconds: config.messagesRevalidateSeconds ?? 30,
|
|
40
32
|
};
|
|
41
33
|
};
|
|
42
34
|
|
|
43
|
-
|
|
44
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Get the project base URL on CDN
|
|
37
|
+
*/
|
|
38
|
+
export const getProjectBaseUrl = (config: NormalizedConfig): string =>
|
|
39
|
+
coreGetProjectBaseUrl(config);
|
package/src/index.ts
CHANGED
|
@@ -1,28 +1,61 @@
|
|
|
1
1
|
import type { I18nConfig } from "./types";
|
|
2
2
|
import { normalizeConfig } from "./config";
|
|
3
3
|
import {
|
|
4
|
+
createNextI18nCore,
|
|
4
5
|
createNextIntlRequestConfig,
|
|
5
|
-
getLocales,
|
|
6
|
-
getManifest,
|
|
7
|
-
getMessages,
|
|
8
6
|
} from "./server";
|
|
9
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
createI18nMiddleware,
|
|
9
|
+
createI18nProxy,
|
|
10
|
+
createBetterI18nMiddleware,
|
|
11
|
+
composeMiddleware,
|
|
12
|
+
} from "./middleware";
|
|
10
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Create a complete i18n setup for Next.js
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* // i18n/config.ts
|
|
20
|
+
* import { createI18n } from '@better-i18n/next'
|
|
21
|
+
*
|
|
22
|
+
* export const i18n = createI18n({
|
|
23
|
+
* project: 'acme/dashboard',
|
|
24
|
+
* defaultLocale: 'en',
|
|
25
|
+
* })
|
|
26
|
+
*
|
|
27
|
+
* // i18n/request.ts
|
|
28
|
+
* export default i18n.requestConfig
|
|
29
|
+
*
|
|
30
|
+
* // middleware.ts
|
|
31
|
+
* export default i18n.middleware
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
11
34
|
export const createI18n = (config: I18nConfig) => {
|
|
12
35
|
const normalized = normalizeConfig(config);
|
|
36
|
+
const i18n = createNextI18nCore(normalized);
|
|
13
37
|
|
|
14
38
|
return {
|
|
15
39
|
config: normalized,
|
|
16
40
|
requestConfig: createNextIntlRequestConfig(normalized),
|
|
17
41
|
middleware: createI18nMiddleware(normalized),
|
|
18
42
|
proxy: createI18nProxy(normalized),
|
|
19
|
-
getManifest:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
getMessages: (locale: string) => getMessages(normalized, locale),
|
|
43
|
+
getManifest: i18n.getManifest,
|
|
44
|
+
getLocales: i18n.getLocales,
|
|
45
|
+
getMessages: i18n.getMessages,
|
|
23
46
|
};
|
|
24
47
|
};
|
|
25
48
|
|
|
49
|
+
// Modern standalone middleware exports
|
|
50
|
+
export { createBetterI18nMiddleware, composeMiddleware };
|
|
51
|
+
|
|
52
|
+
// Core instance factory
|
|
53
|
+
export { createNextI18nCore } from "./server";
|
|
54
|
+
|
|
55
|
+
// Client hook
|
|
56
|
+
export { useManifestLanguages } from "./client";
|
|
57
|
+
|
|
58
|
+
// Re-export types
|
|
26
59
|
export type {
|
|
27
60
|
I18nConfig,
|
|
28
61
|
LanguageOption,
|
|
@@ -33,3 +66,5 @@ export type {
|
|
|
33
66
|
ManifestResponse,
|
|
34
67
|
Messages,
|
|
35
68
|
} from "./types";
|
|
69
|
+
|
|
70
|
+
export type { I18nMiddlewareConfig } from "@better-i18n/core";
|
package/src/middleware.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import createMiddleware from "next-intl/middleware";
|
|
2
|
-
import
|
|
2
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
3
|
+
import { createI18nCore, createLogger, detectLocale } from "@better-i18n/core";
|
|
4
|
+
import type { I18nMiddlewareConfig } from "@better-i18n/core";
|
|
3
5
|
|
|
4
6
|
import { normalizeConfig } from "./config";
|
|
5
|
-
import {
|
|
6
|
-
import { getLocales } from "./core";
|
|
7
|
+
import { getLocales } from "./server";
|
|
7
8
|
import type { I18nConfig } from "./types";
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Legacy Next-intl based middleware
|
|
12
|
+
*/
|
|
9
13
|
export const createI18nMiddleware = (config: I18nConfig) => {
|
|
10
14
|
const normalized = normalizeConfig(config);
|
|
11
15
|
const logger = createLogger(normalized, "middleware");
|
|
@@ -25,3 +29,82 @@ export const createI18nMiddleware = (config: I18nConfig) => {
|
|
|
25
29
|
};
|
|
26
30
|
|
|
27
31
|
export const createI18nProxy = createI18nMiddleware;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Modern composable middleware for Better i18n
|
|
35
|
+
*/
|
|
36
|
+
export function createBetterI18nMiddleware(config: I18nMiddlewareConfig) {
|
|
37
|
+
const { project, defaultLocale, detection = {} } = config;
|
|
38
|
+
|
|
39
|
+
const {
|
|
40
|
+
cookie = true,
|
|
41
|
+
browserLanguage = true,
|
|
42
|
+
cookieName = "locale",
|
|
43
|
+
cookieMaxAge = 31536000,
|
|
44
|
+
} = detection;
|
|
45
|
+
|
|
46
|
+
// Create i18n core instance for CDN operations
|
|
47
|
+
const i18nCore = createI18nCore({ project, defaultLocale });
|
|
48
|
+
|
|
49
|
+
return async (request: NextRequest): Promise<NextResponse> => {
|
|
50
|
+
// 1. Fetch available locales from CDN
|
|
51
|
+
const availableLocales = await i18nCore.getLocales();
|
|
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 with locale header for Server Components
|
|
71
|
+
const response = NextResponse.next({
|
|
72
|
+
headers: {
|
|
73
|
+
"x-locale": result.locale,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// 5. Set cookie if needed
|
|
78
|
+
if (cookie && result.shouldSetCookie) {
|
|
79
|
+
response.cookies.set(cookieName, result.locale, {
|
|
80
|
+
path: "/",
|
|
81
|
+
maxAge: cookieMaxAge,
|
|
82
|
+
sameSite: "lax",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return response;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Helper to compose multiple Next.js middleware
|
|
92
|
+
*/
|
|
93
|
+
export function composeMiddleware(
|
|
94
|
+
...middlewares: Array<(req: NextRequest) => Promise<NextResponse>>
|
|
95
|
+
) {
|
|
96
|
+
return async (request: NextRequest): Promise<NextResponse> => {
|
|
97
|
+
let response = NextResponse.next();
|
|
98
|
+
|
|
99
|
+
for (const middleware of middlewares) {
|
|
100
|
+
response = await middleware(request);
|
|
101
|
+
|
|
102
|
+
// Short-circuit on redirect/rewrite (status >= 300)
|
|
103
|
+
if (response.status >= 300) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return response;
|
|
109
|
+
};
|
|
110
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -1,23 +1,146 @@
|
|
|
1
1
|
import { getRequestConfig } from "next-intl/server";
|
|
2
|
+
import { createI18nCore } from "@better-i18n/core";
|
|
3
|
+
import type { I18nCore, Messages } from "@better-i18n/core";
|
|
2
4
|
|
|
3
|
-
import { normalizeConfig } from "./config";
|
|
4
|
-
import {
|
|
5
|
-
import type { I18nConfig } from "./types";
|
|
5
|
+
import { normalizeConfig, getProjectBaseUrl } from "./config";
|
|
6
|
+
import type { I18nConfig, NextFetchRequestInit } from "./types";
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Next.js i18n core instance with ISR support
|
|
10
|
+
*/
|
|
11
|
+
export interface NextI18nCore extends I18nCore {
|
|
12
|
+
/**
|
|
13
|
+
* Get messages for a locale with Next.js ISR revalidation
|
|
14
|
+
*/
|
|
15
|
+
getMessages: (locale: string) => Promise<Messages>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a fetch function with Next.js ISR revalidation
|
|
20
|
+
*/
|
|
21
|
+
const createIsrFetch = (revalidateSeconds: number): typeof fetch => {
|
|
22
|
+
return (input: RequestInfo | URL, init?: RequestInit) => {
|
|
23
|
+
const nextInit: NextFetchRequestInit = {
|
|
24
|
+
...init,
|
|
25
|
+
next: { revalidate: revalidateSeconds },
|
|
26
|
+
};
|
|
27
|
+
return fetch(input, nextInit);
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a Next.js i18n core instance with ISR support
|
|
33
|
+
*
|
|
34
|
+
* This wraps the framework-agnostic `createI18nCore` from `@better-i18n/core`
|
|
35
|
+
* and adds Next.js-specific ISR revalidation for optimal caching.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const i18n = createNextI18nCore({
|
|
40
|
+
* project: 'acme/dashboard',
|
|
41
|
+
* defaultLocale: 'en',
|
|
42
|
+
* })
|
|
43
|
+
*
|
|
44
|
+
* // Manifest cached for 1 hour (ISR)
|
|
45
|
+
* const locales = await i18n.getLocales()
|
|
46
|
+
*
|
|
47
|
+
* // Messages revalidated every 30s (ISR)
|
|
48
|
+
* const messages = await i18n.getMessages('en')
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export const createNextI18nCore = (config: I18nConfig): NextI18nCore => {
|
|
52
|
+
const normalized = normalizeConfig(config);
|
|
53
|
+
|
|
54
|
+
// Core instance uses ISR fetch for manifest (default 3600s)
|
|
55
|
+
const manifestFetch = createIsrFetch(normalized.manifestRevalidateSeconds);
|
|
56
|
+
const i18nCore = createI18nCore({
|
|
57
|
+
project: normalized.project,
|
|
58
|
+
defaultLocale: normalized.defaultLocale,
|
|
59
|
+
cdnBaseUrl: normalized.cdnBaseUrl,
|
|
60
|
+
manifestCacheTtlMs: normalized.manifestCacheTtlMs,
|
|
61
|
+
debug: normalized.debug,
|
|
62
|
+
logLevel: normalized.logLevel,
|
|
63
|
+
fetch: manifestFetch,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Messages use separate ISR fetch with shorter revalidation (default 30s)
|
|
67
|
+
const messagesFetch = createIsrFetch(normalized.messagesRevalidateSeconds);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
...i18nCore,
|
|
71
|
+
getMessages: async (locale: string): Promise<Messages> => {
|
|
72
|
+
const url = `${getProjectBaseUrl(normalized)}/${locale}/translations.json`;
|
|
73
|
+
const response = await messagesFetch(url);
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new Error(`[better-i18n] Messages fetch failed for locale "${locale}" (${response.status})`);
|
|
76
|
+
}
|
|
77
|
+
return response.json();
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create next-intl request config for App Router
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* // i18n/request.ts
|
|
88
|
+
* import { createNextIntlRequestConfig } from '@better-i18n/next/server'
|
|
89
|
+
*
|
|
90
|
+
* export default createNextIntlRequestConfig({
|
|
91
|
+
* project: 'acme/dashboard',
|
|
92
|
+
* defaultLocale: 'en',
|
|
93
|
+
* })
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
7
96
|
export const createNextIntlRequestConfig = (config: I18nConfig) =>
|
|
8
97
|
getRequestConfig(async ({ requestLocale }) => {
|
|
9
|
-
const
|
|
98
|
+
const i18n = createNextI18nCore(config);
|
|
99
|
+
const normalized = normalizeConfig(config);
|
|
100
|
+
const locales = await i18n.getLocales();
|
|
10
101
|
let locale = await requestLocale;
|
|
11
102
|
|
|
12
103
|
if (!locale || !locales.includes(locale)) {
|
|
13
|
-
locale =
|
|
104
|
+
locale = normalized.defaultLocale;
|
|
14
105
|
}
|
|
15
106
|
|
|
16
107
|
return {
|
|
17
108
|
locale,
|
|
18
|
-
messages: await getMessages(
|
|
109
|
+
messages: await i18n.getMessages(locale),
|
|
19
110
|
};
|
|
20
111
|
});
|
|
21
112
|
|
|
22
|
-
|
|
23
|
-
|
|
113
|
+
// Convenience exports for backwards compatibility
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Fetch manifest from CDN
|
|
117
|
+
*/
|
|
118
|
+
export const fetchManifest = (config: I18nConfig) =>
|
|
119
|
+
createNextI18nCore(config).getManifest();
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get manifest with caching
|
|
123
|
+
*/
|
|
124
|
+
export const getManifest = (config: I18nConfig, options?: { forceRefresh?: boolean }) =>
|
|
125
|
+
createNextI18nCore(config).getManifest(options);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get available locale codes
|
|
129
|
+
*/
|
|
130
|
+
export const getLocales = (config: I18nConfig) =>
|
|
131
|
+
createNextI18nCore(config).getLocales();
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get messages for a locale
|
|
135
|
+
*/
|
|
136
|
+
export const getMessages = (config: I18nConfig, locale: string) =>
|
|
137
|
+
createNextI18nCore(config).getMessages(locale);
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get language options with metadata
|
|
141
|
+
*/
|
|
142
|
+
export const getManifestLanguages = (config: I18nConfig) =>
|
|
143
|
+
createNextI18nCore(config).getLanguages();
|
|
144
|
+
|
|
145
|
+
// Re-export types from core
|
|
146
|
+
export type { LanguageOption, ManifestLanguage, ManifestResponse, Messages } from "@better-i18n/core";
|
package/src/types.ts
CHANGED
|
@@ -1,74 +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;
|
|
17
|
-
messagesRevalidateSeconds?: number;
|
|
18
|
-
fetch?: typeof fetch;
|
|
19
|
-
}
|
|
20
35
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Next.js ISR revalidation time for messages (seconds)
|
|
38
|
+
* @default 30
|
|
39
|
+
*/
|
|
40
|
+
messagesRevalidateSeconds?: number;
|
|
25
41
|
}
|
|
26
42
|
|
|
27
|
-
|
|
28
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Normalized Next.js config with defaults
|
|
45
|
+
*/
|
|
46
|
+
export interface NormalizedConfig extends CoreNormalizedConfig, Omit<I18nConfig, keyof I18nCoreConfig> {
|
|
29
47
|
localePrefix: LocalePrefix;
|
|
48
|
+
manifestRevalidateSeconds: number;
|
|
49
|
+
messagesRevalidateSeconds: number;
|
|
30
50
|
}
|
|
31
51
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
nativeName?: string;
|
|
36
|
-
flagUrl?: string | null;
|
|
37
|
-
isSource?: boolean;
|
|
38
|
-
lastUpdated?: string | null;
|
|
39
|
-
keyCount?: number;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface ManifestFile {
|
|
43
|
-
url: string;
|
|
44
|
-
size: number;
|
|
45
|
-
lastModified: string | null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface ManifestResponse {
|
|
49
|
-
projectSlug?: string;
|
|
50
|
-
sourceLanguage?: string;
|
|
51
|
-
languages: ManifestLanguage[];
|
|
52
|
-
files?: Record<string, ManifestFile>;
|
|
53
|
-
updatedAt?: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface LanguageOption {
|
|
57
|
-
code: string;
|
|
58
|
-
name?: string;
|
|
59
|
-
nativeName?: string;
|
|
60
|
-
flagUrl?: string | null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export type Messages = Record<string, any>;
|
|
64
|
-
|
|
65
|
-
export interface Logger {
|
|
66
|
-
debug: (...args: unknown[]) => void;
|
|
67
|
-
info: (...args: unknown[]) => void;
|
|
68
|
-
warn: (...args: unknown[]) => void;
|
|
69
|
-
error: (...args: unknown[]) => void;
|
|
70
|
-
}
|
|
71
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Next.js fetch request init with ISR options
|
|
54
|
+
*/
|
|
72
55
|
export type NextFetchRequestInit = RequestInit & {
|
|
73
56
|
next?: {
|
|
74
57
|
revalidate?: number;
|
package/src/core.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { getProjectBaseUrl, normalizeConfig } from "./config";
|
|
2
|
-
import { createLogger } from "./logger";
|
|
3
|
-
import { extractLanguages } from "./manifest";
|
|
4
|
-
import type {
|
|
5
|
-
I18nConfig,
|
|
6
|
-
LanguageOption,
|
|
7
|
-
ManifestResponse,
|
|
8
|
-
Messages,
|
|
9
|
-
NextFetchRequestInit,
|
|
10
|
-
NormalizedConfig,
|
|
11
|
-
} from "./types";
|
|
12
|
-
|
|
13
|
-
type ManifestCacheEntry = {
|
|
14
|
-
value: ManifestResponse;
|
|
15
|
-
expiresAt: number;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const manifestCache = new Map<string, ManifestCacheEntry>();
|
|
19
|
-
|
|
20
|
-
const buildCacheKey = (config: NormalizedConfig) =>
|
|
21
|
-
`${config.cdnBaseUrl}|${config.project}`;
|
|
22
|
-
|
|
23
|
-
const withRevalidate = (
|
|
24
|
-
init: NextFetchRequestInit,
|
|
25
|
-
revalidateSeconds?: number,
|
|
26
|
-
): NextFetchRequestInit => {
|
|
27
|
-
if (!revalidateSeconds || revalidateSeconds <= 0) {
|
|
28
|
-
return init;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const { cache, ...rest } = init;
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
...rest,
|
|
35
|
-
next: {
|
|
36
|
-
...init.next,
|
|
37
|
-
revalidate: revalidateSeconds,
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export const fetchManifest = async (
|
|
43
|
-
config: I18nConfig,
|
|
44
|
-
init: NextFetchRequestInit = {},
|
|
45
|
-
): Promise<ManifestResponse> => {
|
|
46
|
-
const normalized = normalizeConfig(config);
|
|
47
|
-
const logger = createLogger(normalized, "manifest");
|
|
48
|
-
const url = `${getProjectBaseUrl(normalized)}/manifest.json`;
|
|
49
|
-
const requestInit = withRevalidate(
|
|
50
|
-
{
|
|
51
|
-
...init,
|
|
52
|
-
},
|
|
53
|
-
normalized.manifestRevalidateSeconds,
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
logger.debug("fetch", url);
|
|
57
|
-
|
|
58
|
-
const fetchFn = normalized.fetch ?? fetch;
|
|
59
|
-
const response = await fetchFn(url, requestInit);
|
|
60
|
-
|
|
61
|
-
if (!response.ok) {
|
|
62
|
-
const message = `[better-i18n] Manifest fetch failed (${response.status})`;
|
|
63
|
-
logger.error(message);
|
|
64
|
-
throw new Error(message);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const data = (await response.json()) as ManifestResponse;
|
|
68
|
-
if (!Array.isArray(data.languages)) {
|
|
69
|
-
throw new Error("[better-i18n] Manifest payload missing languages array");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
logger.debug("fetched", { languages: data.languages.length });
|
|
73
|
-
return data;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
export const getManifest = async (
|
|
77
|
-
config: I18nConfig,
|
|
78
|
-
options: { forceRefresh?: boolean } = {},
|
|
79
|
-
): Promise<ManifestResponse> => {
|
|
80
|
-
const normalized = normalizeConfig(config);
|
|
81
|
-
const cacheKey = buildCacheKey(normalized);
|
|
82
|
-
const now = Date.now();
|
|
83
|
-
const cached = manifestCache.get(cacheKey);
|
|
84
|
-
|
|
85
|
-
if (!options.forceRefresh && cached && cached.expiresAt > now) {
|
|
86
|
-
return cached.value;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const manifest = await fetchManifest(normalized);
|
|
90
|
-
manifestCache.set(cacheKey, {
|
|
91
|
-
value: manifest,
|
|
92
|
-
expiresAt: now + (normalized.manifestCacheTtlMs ?? 0),
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
return manifest;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
export const getLocales = async (config: I18nConfig): Promise<string[]> => {
|
|
99
|
-
const manifest = await getManifest(config);
|
|
100
|
-
const languages = extractLanguages(manifest);
|
|
101
|
-
|
|
102
|
-
if (languages.length === 0) {
|
|
103
|
-
throw new Error("[better-i18n] No locales found in manifest");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return languages.map((language) => language.code);
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
export const getMessages = async (
|
|
110
|
-
config: I18nConfig,
|
|
111
|
-
locale: string,
|
|
112
|
-
): Promise<Messages> => {
|
|
113
|
-
const normalized = normalizeConfig(config);
|
|
114
|
-
const logger = createLogger(normalized, "messages");
|
|
115
|
-
const url = `${getProjectBaseUrl(normalized)}/${locale}/translations.json`;
|
|
116
|
-
|
|
117
|
-
const requestInit = withRevalidate(
|
|
118
|
-
{},
|
|
119
|
-
normalized.messagesRevalidateSeconds,
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
logger.debug("fetch", url);
|
|
123
|
-
|
|
124
|
-
const fetchFn = normalized.fetch ?? fetch;
|
|
125
|
-
const response = await fetchFn(url, requestInit);
|
|
126
|
-
|
|
127
|
-
if (!response.ok) {
|
|
128
|
-
const message = `[better-i18n] Messages fetch failed (${response.status})`;
|
|
129
|
-
logger.error(message);
|
|
130
|
-
throw new Error(message);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return (await response.json()) as Messages;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
export const getManifestLanguages = async (
|
|
137
|
-
config: I18nConfig,
|
|
138
|
-
): Promise<LanguageOption[]> => {
|
|
139
|
-
const manifest = await getManifest(config);
|
|
140
|
-
const languages = extractLanguages(manifest);
|
|
141
|
-
|
|
142
|
-
if (languages.length === 0) {
|
|
143
|
-
throw new Error("[better-i18n] No languages found in manifest");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return languages;
|
|
147
|
-
};
|
package/src/logger.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import type { I18nConfig, Logger, LogLevel } from "./types";
|
|
2
|
-
|
|
3
|
-
const LEVEL_RANK: Record<LogLevel, number> = {
|
|
4
|
-
debug: 0,
|
|
5
|
-
info: 1,
|
|
6
|
-
warn: 2,
|
|
7
|
-
error: 3,
|
|
8
|
-
silent: 4,
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const getEnv = (key: string): string | undefined => {
|
|
12
|
-
if (typeof process === "undefined" || !process.env) {
|
|
13
|
-
return undefined;
|
|
14
|
-
}
|
|
15
|
-
return process.env[key];
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const normalizeLevel = (level?: string): LogLevel | undefined => {
|
|
19
|
-
if (!level) return undefined;
|
|
20
|
-
const normalized = level.toLowerCase();
|
|
21
|
-
if (normalized in LEVEL_RANK) {
|
|
22
|
-
return normalized as LogLevel;
|
|
23
|
-
}
|
|
24
|
-
return undefined;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const resolveLevel = (config: I18nConfig): LogLevel => {
|
|
28
|
-
const envDebug = getEnv("BETTER_I18N_DEBUG");
|
|
29
|
-
if (envDebug && envDebug !== "0" && envDebug.toLowerCase() !== "false") {
|
|
30
|
-
return "debug";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const envLevel = normalizeLevel(getEnv("BETTER_I18N_LOG_LEVEL"));
|
|
34
|
-
if (envLevel) return envLevel;
|
|
35
|
-
|
|
36
|
-
if (config.logLevel) return config.logLevel;
|
|
37
|
-
if (config.debug) return "debug";
|
|
38
|
-
|
|
39
|
-
return "warn";
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const shouldLog = (current: LogLevel, target: LogLevel) =>
|
|
43
|
-
LEVEL_RANK[target] >= LEVEL_RANK[current];
|
|
44
|
-
|
|
45
|
-
export const createLogger = (config: I18nConfig, scope = "core"): Logger => {
|
|
46
|
-
const level = resolveLevel(config);
|
|
47
|
-
const prefix = `[better-i18n:${scope}]`;
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
debug: (...args: unknown[]) => {
|
|
51
|
-
if (!shouldLog(level, "debug")) return;
|
|
52
|
-
console.debug(prefix, ...args);
|
|
53
|
-
},
|
|
54
|
-
info: (...args: unknown[]) => {
|
|
55
|
-
if (!shouldLog(level, "info")) return;
|
|
56
|
-
console.info(prefix, ...args);
|
|
57
|
-
},
|
|
58
|
-
warn: (...args: unknown[]) => {
|
|
59
|
-
if (!shouldLog(level, "warn")) return;
|
|
60
|
-
console.warn(prefix, ...args);
|
|
61
|
-
},
|
|
62
|
-
error: (...args: unknown[]) => {
|
|
63
|
-
if (!shouldLog(level, "error")) return;
|
|
64
|
-
console.error(prefix, ...args);
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
};
|
package/src/manifest.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
LanguageOption,
|
|
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
|
-
};
|