@better-i18n/next 0.1.3 → 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/package.json +1 -1
- package/src/client.ts +80 -90
- package/src/index.ts +43 -8
- package/src/middleware.ts +12 -8
- package/src/server.ts +131 -8
- package/src/core.ts +0 -147
- package/src/logger.ts +0 -68
- package/src/manifest.ts +0 -2
package/package.json
CHANGED
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/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,13 +1,10 @@
|
|
|
1
1
|
import createMiddleware from "next-intl/middleware";
|
|
2
2
|
import { NextRequest, NextResponse } from "next/server";
|
|
3
|
-
|
|
4
|
-
import { detectLocale, getLocales as getCDNLocales } from "@better-i18n/core";
|
|
5
|
-
// @ts-ignore - internal workspace dependency
|
|
3
|
+
import { createI18nCore, createLogger, detectLocale } from "@better-i18n/core";
|
|
6
4
|
import type { I18nMiddlewareConfig } from "@better-i18n/core";
|
|
7
5
|
|
|
8
6
|
import { normalizeConfig } from "./config";
|
|
9
|
-
import {
|
|
10
|
-
import { getLocales } from "./core";
|
|
7
|
+
import { getLocales } from "./server";
|
|
11
8
|
import type { I18nConfig } from "./types";
|
|
12
9
|
|
|
13
10
|
/**
|
|
@@ -46,9 +43,12 @@ export function createBetterI18nMiddleware(config: I18nMiddlewareConfig) {
|
|
|
46
43
|
cookieMaxAge = 31536000,
|
|
47
44
|
} = detection;
|
|
48
45
|
|
|
46
|
+
// Create i18n core instance for CDN operations
|
|
47
|
+
const i18nCore = createI18nCore({ project, defaultLocale });
|
|
48
|
+
|
|
49
49
|
return async (request: NextRequest): Promise<NextResponse> => {
|
|
50
50
|
// 1. Fetch available locales from CDN
|
|
51
|
-
const availableLocales = await
|
|
51
|
+
const availableLocales = await i18nCore.getLocales();
|
|
52
52
|
|
|
53
53
|
// 2. Extract locale indicators
|
|
54
54
|
const pathLocale = request.nextUrl.pathname.split("/")[1];
|
|
@@ -67,8 +67,12 @@ export function createBetterI18nMiddleware(config: I18nMiddlewareConfig) {
|
|
|
67
67
|
availableLocales,
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
// 4. Create response
|
|
71
|
-
const response = NextResponse.next(
|
|
70
|
+
// 4. Create response with locale header for Server Components
|
|
71
|
+
const response = NextResponse.next({
|
|
72
|
+
headers: {
|
|
73
|
+
"x-locale": result.locale,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
72
76
|
|
|
73
77
|
// 5. Set cookie if needed
|
|
74
78
|
if (cookie && result.shouldSetCookie) {
|
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/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,68 +0,0 @@
|
|
|
1
|
-
import type { Logger, LogLevel } from "@better-i18n/core";
|
|
2
|
-
import type { I18nConfig } from "./types";
|
|
3
|
-
|
|
4
|
-
const LEVEL_RANK: Record<LogLevel, number> = {
|
|
5
|
-
debug: 0,
|
|
6
|
-
info: 1,
|
|
7
|
-
warn: 2,
|
|
8
|
-
error: 3,
|
|
9
|
-
silent: 4,
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const getEnv = (key: string): string | undefined => {
|
|
13
|
-
if (typeof process === "undefined" || !process.env) {
|
|
14
|
-
return undefined;
|
|
15
|
-
}
|
|
16
|
-
return process.env[key];
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const normalizeLevel = (level?: string): LogLevel | undefined => {
|
|
20
|
-
if (!level) return undefined;
|
|
21
|
-
const normalized = level.toLowerCase();
|
|
22
|
-
if (normalized in LEVEL_RANK) {
|
|
23
|
-
return normalized as LogLevel;
|
|
24
|
-
}
|
|
25
|
-
return undefined;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const resolveLevel = (config: I18nConfig): LogLevel => {
|
|
29
|
-
const envDebug = getEnv("BETTER_I18N_DEBUG");
|
|
30
|
-
if (envDebug && envDebug !== "0" && envDebug.toLowerCase() !== "false") {
|
|
31
|
-
return "debug";
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const envLevel = normalizeLevel(getEnv("BETTER_I18N_LOG_LEVEL"));
|
|
35
|
-
if (envLevel) return envLevel;
|
|
36
|
-
|
|
37
|
-
if (config.logLevel) return config.logLevel;
|
|
38
|
-
if (config.debug) return "debug";
|
|
39
|
-
|
|
40
|
-
return "warn";
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const shouldLog = (current: LogLevel, target: LogLevel) =>
|
|
44
|
-
LEVEL_RANK[target] >= LEVEL_RANK[current];
|
|
45
|
-
|
|
46
|
-
export const createLogger = (config: I18nConfig, scope = "core"): Logger => {
|
|
47
|
-
const level = resolveLevel(config);
|
|
48
|
-
const prefix = `[better-i18n:${scope}]`;
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
debug: (...args: unknown[]) => {
|
|
52
|
-
if (!shouldLog(level, "debug")) return;
|
|
53
|
-
console.debug(prefix, ...args);
|
|
54
|
-
},
|
|
55
|
-
info: (...args: unknown[]) => {
|
|
56
|
-
if (!shouldLog(level, "info")) return;
|
|
57
|
-
console.info(prefix, ...args);
|
|
58
|
-
},
|
|
59
|
-
warn: (...args: unknown[]) => {
|
|
60
|
-
if (!shouldLog(level, "warn")) return;
|
|
61
|
-
console.warn(prefix, ...args);
|
|
62
|
-
},
|
|
63
|
-
error: (...args: unknown[]) => {
|
|
64
|
-
if (!shouldLog(level, "error")) return;
|
|
65
|
-
console.error(prefix, ...args);
|
|
66
|
-
},
|
|
67
|
-
};
|
|
68
|
-
};
|
package/src/manifest.ts
DELETED