@better-i18n/use-intl 0.1.2 → 0.1.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/package.json +3 -4
- package/src/components.tsx +76 -0
- package/src/context.tsx +43 -0
- package/src/hooks.ts +70 -0
- package/src/index.ts +30 -0
- package/src/middleware/index.ts +61 -0
- package/src/provider.tsx +196 -0
- package/src/server.ts +108 -0
- package/src/types.ts +90 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-i18n/use-intl",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Better i18n integration for use-intl (React, TanStack Start)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"./package.json": "./package.json"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
-
"
|
|
25
|
+
"src",
|
|
26
26
|
"package.json",
|
|
27
27
|
"README.md"
|
|
28
28
|
],
|
|
@@ -44,14 +44,13 @@
|
|
|
44
44
|
"clean": "rm -rf dist"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@better-i18n/core": "
|
|
47
|
+
"@better-i18n/core": "0.1.4"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"react": ">=18.0.0",
|
|
51
51
|
"use-intl": ">=4.0.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@better-i18n/typescript-config": "workspace:*",
|
|
55
54
|
"@types/react": "^19.0.0",
|
|
56
55
|
"react": "^19.0.0",
|
|
57
56
|
"use-intl": "^4.7.0",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
4
|
+
import { useBetterI18n } from "./context";
|
|
5
|
+
|
|
6
|
+
export interface LanguageSwitcherProps extends Omit<
|
|
7
|
+
ComponentProps<"select">,
|
|
8
|
+
"value" | "onChange" | "children"
|
|
9
|
+
> {
|
|
10
|
+
/**
|
|
11
|
+
* Render function for custom option display
|
|
12
|
+
*/
|
|
13
|
+
renderOption?: (language: {
|
|
14
|
+
code: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
nativeName?: string;
|
|
17
|
+
flagUrl?: string | null;
|
|
18
|
+
}) => ReactNode;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Label for loading state
|
|
22
|
+
*/
|
|
23
|
+
loadingLabel?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Pre-built language switcher component
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* // Basic usage
|
|
32
|
+
* <LanguageSwitcher />
|
|
33
|
+
*
|
|
34
|
+
* // With custom styling
|
|
35
|
+
* <LanguageSwitcher className="my-select" />
|
|
36
|
+
*
|
|
37
|
+
* // Custom option rendering
|
|
38
|
+
* <LanguageSwitcher
|
|
39
|
+
* renderOption={(lang) => (
|
|
40
|
+
* <>
|
|
41
|
+
* {lang.flagUrl && <img src={lang.flagUrl} alt="" />}
|
|
42
|
+
* {lang.nativeName}
|
|
43
|
+
* </>
|
|
44
|
+
* )}
|
|
45
|
+
* />
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function LanguageSwitcher({
|
|
49
|
+
renderOption,
|
|
50
|
+
loadingLabel = "Loading...",
|
|
51
|
+
...props
|
|
52
|
+
}: LanguageSwitcherProps) {
|
|
53
|
+
const { locale, setLocale, languages, isLoadingLanguages } = useBetterI18n();
|
|
54
|
+
|
|
55
|
+
if (isLoadingLanguages) {
|
|
56
|
+
return (
|
|
57
|
+
<select disabled {...props}>
|
|
58
|
+
<option>{loadingLabel}</option>
|
|
59
|
+
</select>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<select
|
|
65
|
+
value={locale}
|
|
66
|
+
onChange={(e) => setLocale(e.target.value)}
|
|
67
|
+
{...props}
|
|
68
|
+
>
|
|
69
|
+
{languages.map((lang) => (
|
|
70
|
+
<option key={lang.code} value={lang.code}>
|
|
71
|
+
{renderOption ? renderOption(lang) : lang.nativeName || lang.code}
|
|
72
|
+
</option>
|
|
73
|
+
))}
|
|
74
|
+
</select>
|
|
75
|
+
);
|
|
76
|
+
}
|
package/src/context.tsx
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
import type { BetterI18nContextValue } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Context for Better i18n specific state
|
|
8
|
+
*/
|
|
9
|
+
export const BetterI18nContext = createContext<BetterI18nContextValue | null>(
|
|
10
|
+
null
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Hook to access Better i18n context
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* function LanguageSwitcher() {
|
|
19
|
+
* const { locale, setLocale, languages } = useBetterI18n()
|
|
20
|
+
*
|
|
21
|
+
* return (
|
|
22
|
+
* <select value={locale} onChange={(e) => setLocale(e.target.value)}>
|
|
23
|
+
* {languages.map((lang) => (
|
|
24
|
+
* <option key={lang.code} value={lang.code}>
|
|
25
|
+
* {lang.nativeName}
|
|
26
|
+
* </option>
|
|
27
|
+
* ))}
|
|
28
|
+
* </select>
|
|
29
|
+
* )
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function useBetterI18n(): BetterI18nContextValue {
|
|
34
|
+
const context = useContext(BetterI18nContext);
|
|
35
|
+
|
|
36
|
+
if (!context) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
"[better-i18n] useBetterI18n must be used within a BetterI18nProvider"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return context;
|
|
43
|
+
}
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useBetterI18n } from "./context";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook to get available languages
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* function LanguageList() {
|
|
11
|
+
* const { languages, isLoading } = useLanguages()
|
|
12
|
+
*
|
|
13
|
+
* if (isLoading) return <div>Loading...</div>
|
|
14
|
+
*
|
|
15
|
+
* return (
|
|
16
|
+
* <ul>
|
|
17
|
+
* {languages.map((lang) => (
|
|
18
|
+
* <li key={lang.code}>
|
|
19
|
+
* {lang.nativeName} ({lang.code})
|
|
20
|
+
* </li>
|
|
21
|
+
* ))}
|
|
22
|
+
* </ul>
|
|
23
|
+
* )
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function useLanguages() {
|
|
28
|
+
const { languages, isLoadingLanguages } = useBetterI18n();
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
languages,
|
|
32
|
+
isLoading: isLoadingLanguages,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Hook to get and set current locale
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* function LocaleSwitcher() {
|
|
42
|
+
* const { locale, setLocale } = useLocale()
|
|
43
|
+
*
|
|
44
|
+
* return (
|
|
45
|
+
* <button onClick={() => setLocale(locale === 'en' ? 'tr' : 'en')}>
|
|
46
|
+
* Current: {locale}
|
|
47
|
+
* </button>
|
|
48
|
+
* )
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function useLocale() {
|
|
53
|
+
const { locale, setLocale, isLoadingMessages } = useBetterI18n();
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
locale,
|
|
57
|
+
setLocale,
|
|
58
|
+
isLoading: isLoadingMessages,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Re-export use-intl hooks for convenience
|
|
63
|
+
export {
|
|
64
|
+
useTranslations,
|
|
65
|
+
useFormatter,
|
|
66
|
+
useMessages,
|
|
67
|
+
useNow,
|
|
68
|
+
useTimeZone,
|
|
69
|
+
useLocale as useIntlLocale,
|
|
70
|
+
} from "use-intl";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Provider
|
|
2
|
+
export { BetterI18nProvider } from "./provider";
|
|
3
|
+
export type { BetterI18nProviderProps } from "./provider";
|
|
4
|
+
|
|
5
|
+
// Context & Hooks
|
|
6
|
+
export { useBetterI18n } from "./context";
|
|
7
|
+
export {
|
|
8
|
+
useLanguages,
|
|
9
|
+
useLocale,
|
|
10
|
+
// Re-exported from use-intl
|
|
11
|
+
useTranslations,
|
|
12
|
+
useFormatter,
|
|
13
|
+
useMessages,
|
|
14
|
+
useNow,
|
|
15
|
+
useTimeZone,
|
|
16
|
+
} from "./hooks";
|
|
17
|
+
|
|
18
|
+
// Components
|
|
19
|
+
export { LanguageSwitcher } from "./components";
|
|
20
|
+
export type { LanguageSwitcherProps } from "./components";
|
|
21
|
+
|
|
22
|
+
// Types
|
|
23
|
+
export type {
|
|
24
|
+
Messages,
|
|
25
|
+
BetterI18nProviderConfig,
|
|
26
|
+
BetterI18nContextValue,
|
|
27
|
+
} from "./types";
|
|
28
|
+
|
|
29
|
+
// Re-export commonly used use-intl components
|
|
30
|
+
export { IntlProvider } from "use-intl";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// @ts-ignore - internal workspace dependency
|
|
2
|
+
import { createMiddleware } from "@tanstack/react-router";
|
|
3
|
+
// @ts-ignore - internal workspace dependency
|
|
4
|
+
import { detectLocale, getLocales } from "@better-i18n/core";
|
|
5
|
+
// @ts-ignore - internal workspace dependency
|
|
6
|
+
import type { I18nMiddlewareConfig } from "@better-i18n/core";
|
|
7
|
+
|
|
8
|
+
export function createBetterI18nMiddleware(config: I18nMiddlewareConfig) {
|
|
9
|
+
const { project, defaultLocale, detection = {} } = config;
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
cookie = true,
|
|
13
|
+
browserLanguage = true,
|
|
14
|
+
cookieName = "locale",
|
|
15
|
+
cookieMaxAge = 31536000,
|
|
16
|
+
} = detection;
|
|
17
|
+
|
|
18
|
+
return createMiddleware().server(
|
|
19
|
+
async ({ next, request }: { next: any; request: any }) => {
|
|
20
|
+
// 1. Fetch available locales from CDN (cached)
|
|
21
|
+
const availableLocales = await getLocales({ project });
|
|
22
|
+
|
|
23
|
+
// 2. Extract locale indicators
|
|
24
|
+
const url = new URL(request.url);
|
|
25
|
+
const pathLocale = url.pathname.split("/")[1];
|
|
26
|
+
|
|
27
|
+
// Dynamic imports for TanStack Start server functions to avoid bundling them in client
|
|
28
|
+
// @ts-ignore - internal workspace dependency
|
|
29
|
+
const { getCookie, setCookie, getRequestHeader } =
|
|
30
|
+
// @ts-ignore - dynamic import type safety
|
|
31
|
+
await import("@tanstack/react-start/server");
|
|
32
|
+
|
|
33
|
+
const cookieLocale = cookie ? getCookie(cookieName) : null;
|
|
34
|
+
const headerLocale = browserLanguage
|
|
35
|
+
? getRequestHeader("accept-language")?.split(",")[0]?.split("-")[0]
|
|
36
|
+
: null;
|
|
37
|
+
|
|
38
|
+
// 3. Detect locale using core logic
|
|
39
|
+
const result = detectLocale({
|
|
40
|
+
project,
|
|
41
|
+
defaultLocale,
|
|
42
|
+
pathLocale,
|
|
43
|
+
cookieLocale,
|
|
44
|
+
headerLocale,
|
|
45
|
+
availableLocales,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// 4. Set cookie if needed (if enabled and changed)
|
|
49
|
+
if (cookie && result.shouldSetCookie) {
|
|
50
|
+
setCookie(cookieName, result.locale, {
|
|
51
|
+
path: "/",
|
|
52
|
+
maxAge: cookieMaxAge,
|
|
53
|
+
sameSite: "lax",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 5. Pass locale to route context
|
|
58
|
+
return next({ context: { locale: result.locale } });
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
}
|
package/src/provider.tsx
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createI18nCore } from "@better-i18n/core";
|
|
4
|
+
import type { LanguageOption } from "@better-i18n/core";
|
|
5
|
+
import {
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useState,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { IntlProvider } from "use-intl";
|
|
13
|
+
import { BetterI18nContext } from "./context.js";
|
|
14
|
+
import type { BetterI18nProviderConfig, Messages } from "./types.js";
|
|
15
|
+
|
|
16
|
+
export interface BetterI18nProviderProps extends BetterI18nProviderConfig {
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Provider component that combines Better i18n CDN with use-intl
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* // Basic usage (CSR - fetches messages on client)
|
|
26
|
+
* function App() {
|
|
27
|
+
* return (
|
|
28
|
+
* <BetterI18nProvider
|
|
29
|
+
* project="acme/dashboard"
|
|
30
|
+
* locale="en"
|
|
31
|
+
* >
|
|
32
|
+
* <MyComponent />
|
|
33
|
+
* </BetterI18nProvider>
|
|
34
|
+
* )
|
|
35
|
+
* }
|
|
36
|
+
*
|
|
37
|
+
* // SSR usage (pre-loaded messages)
|
|
38
|
+
* function App({ locale, messages }) {
|
|
39
|
+
* return (
|
|
40
|
+
* <BetterI18nProvider
|
|
41
|
+
* project="acme/dashboard"
|
|
42
|
+
* locale={locale}
|
|
43
|
+
* messages={messages}
|
|
44
|
+
* >
|
|
45
|
+
* <MyComponent />
|
|
46
|
+
* </BetterI18nProvider>
|
|
47
|
+
* )
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function BetterI18nProvider({
|
|
52
|
+
children,
|
|
53
|
+
project,
|
|
54
|
+
locale: initialLocale,
|
|
55
|
+
messages: initialMessages,
|
|
56
|
+
timeZone,
|
|
57
|
+
now,
|
|
58
|
+
onLocaleChange,
|
|
59
|
+
onError,
|
|
60
|
+
cdnBaseUrl,
|
|
61
|
+
debug,
|
|
62
|
+
logLevel,
|
|
63
|
+
fetch: customFetch,
|
|
64
|
+
}: BetterI18nProviderProps) {
|
|
65
|
+
const [locale, setLocaleState] = useState(initialLocale);
|
|
66
|
+
const [messages, setMessages] = useState<Messages | undefined>(
|
|
67
|
+
initialMessages,
|
|
68
|
+
);
|
|
69
|
+
const [languages, setLanguages] = useState<LanguageOption[]>([]);
|
|
70
|
+
const [isLoadingMessages, setIsLoadingMessages] = useState(!initialMessages);
|
|
71
|
+
const [isLoadingLanguages, setIsLoadingLanguages] = useState(true);
|
|
72
|
+
|
|
73
|
+
// Create i18n core instance
|
|
74
|
+
const i18nCore = useMemo(
|
|
75
|
+
() =>
|
|
76
|
+
createI18nCore({
|
|
77
|
+
project,
|
|
78
|
+
defaultLocale: initialLocale,
|
|
79
|
+
cdnBaseUrl,
|
|
80
|
+
debug,
|
|
81
|
+
logLevel,
|
|
82
|
+
fetch: customFetch,
|
|
83
|
+
}),
|
|
84
|
+
[project, initialLocale, cdnBaseUrl, debug, logLevel, customFetch],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Load languages on mount
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
let cancelled = false;
|
|
90
|
+
|
|
91
|
+
const loadLanguages = async () => {
|
|
92
|
+
try {
|
|
93
|
+
const langs = await i18nCore.getLanguages();
|
|
94
|
+
if (!cancelled) {
|
|
95
|
+
setLanguages(langs);
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error("[better-i18n] Failed to load languages:", error);
|
|
99
|
+
} finally {
|
|
100
|
+
if (!cancelled) {
|
|
101
|
+
setIsLoadingLanguages(false);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
loadLanguages();
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
cancelled = true;
|
|
110
|
+
};
|
|
111
|
+
}, [i18nCore]);
|
|
112
|
+
|
|
113
|
+
// Load messages when locale changes (if not pre-loaded)
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
// Skip if we have initial messages for the current locale
|
|
116
|
+
if (initialMessages && locale === initialLocale) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let cancelled = false;
|
|
121
|
+
|
|
122
|
+
const loadMessages = async () => {
|
|
123
|
+
setIsLoadingMessages(true);
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const msgs = await i18nCore.getMessages(locale);
|
|
127
|
+
if (!cancelled) {
|
|
128
|
+
setMessages(msgs as Messages);
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error(
|
|
132
|
+
`[better-i18n] Failed to load messages for locale "${locale}":`,
|
|
133
|
+
error,
|
|
134
|
+
);
|
|
135
|
+
} finally {
|
|
136
|
+
if (!cancelled) {
|
|
137
|
+
setIsLoadingMessages(false);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
loadMessages();
|
|
143
|
+
|
|
144
|
+
return () => {
|
|
145
|
+
cancelled = true;
|
|
146
|
+
};
|
|
147
|
+
}, [locale, i18nCore, initialMessages, initialLocale]);
|
|
148
|
+
|
|
149
|
+
// Locale change handler
|
|
150
|
+
const setLocale = useCallback(
|
|
151
|
+
(newLocale: string) => {
|
|
152
|
+
setLocaleState(newLocale);
|
|
153
|
+
onLocaleChange?.(newLocale);
|
|
154
|
+
},
|
|
155
|
+
[onLocaleChange],
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Context value
|
|
159
|
+
const contextValue = useMemo(
|
|
160
|
+
() => ({
|
|
161
|
+
locale,
|
|
162
|
+
setLocale,
|
|
163
|
+
languages,
|
|
164
|
+
isLoadingLanguages,
|
|
165
|
+
isLoadingMessages,
|
|
166
|
+
project,
|
|
167
|
+
}),
|
|
168
|
+
[
|
|
169
|
+
locale,
|
|
170
|
+
setLocale,
|
|
171
|
+
languages,
|
|
172
|
+
isLoadingLanguages,
|
|
173
|
+
isLoadingMessages,
|
|
174
|
+
project,
|
|
175
|
+
],
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Don't render until we have messages
|
|
179
|
+
if (!messages) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<BetterI18nContext.Provider value={contextValue}>
|
|
185
|
+
<IntlProvider
|
|
186
|
+
locale={locale}
|
|
187
|
+
messages={messages}
|
|
188
|
+
timeZone={timeZone}
|
|
189
|
+
now={now}
|
|
190
|
+
onError={onError}
|
|
191
|
+
>
|
|
192
|
+
{children}
|
|
193
|
+
</IntlProvider>
|
|
194
|
+
</BetterI18nContext.Provider>
|
|
195
|
+
);
|
|
196
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createI18nCore } from "@better-i18n/core";
|
|
2
|
+
import type { I18nCoreConfig, LanguageOption } from "@better-i18n/core";
|
|
3
|
+
import { createFormatter, createTranslator } from "use-intl/core";
|
|
4
|
+
import type { Messages } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export interface GetMessagesConfig extends Omit<
|
|
7
|
+
I18nCoreConfig,
|
|
8
|
+
"defaultLocale"
|
|
9
|
+
> {
|
|
10
|
+
/**
|
|
11
|
+
* Locale to fetch messages for
|
|
12
|
+
*/
|
|
13
|
+
locale: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fetch messages for a locale (server-side)
|
|
18
|
+
*/
|
|
19
|
+
export async function getMessages(
|
|
20
|
+
config: GetMessagesConfig,
|
|
21
|
+
): Promise<Messages> {
|
|
22
|
+
const i18n = createI18nCore({
|
|
23
|
+
project: config.project,
|
|
24
|
+
defaultLocale: config.locale,
|
|
25
|
+
cdnBaseUrl: config.cdnBaseUrl,
|
|
26
|
+
debug: config.debug,
|
|
27
|
+
logLevel: config.logLevel,
|
|
28
|
+
fetch: config.fetch,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const messages = (await i18n.getMessages(config.locale)) as any;
|
|
32
|
+
|
|
33
|
+
// better-i18n convention: JSON matches exact namespace structure.
|
|
34
|
+
// if CDN returns { "hero": { "title": "..." } }, use-intl expects exactly that
|
|
35
|
+
// if it's deeply nested, use-intl also handles nested objects.
|
|
36
|
+
|
|
37
|
+
return messages as Messages;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Fetch available locales (server-side)
|
|
42
|
+
*/
|
|
43
|
+
export async function getLocales(
|
|
44
|
+
config: Omit<I18nCoreConfig, "defaultLocale"> & { defaultLocale?: string },
|
|
45
|
+
): Promise<string[]> {
|
|
46
|
+
const i18n = createI18nCore({
|
|
47
|
+
project: config.project,
|
|
48
|
+
defaultLocale: config.defaultLocale || "en",
|
|
49
|
+
cdnBaseUrl: config.cdnBaseUrl,
|
|
50
|
+
debug: config.debug,
|
|
51
|
+
logLevel: config.logLevel,
|
|
52
|
+
fetch: config.fetch,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return i18n.getLocales();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Fetch available languages with metadata (server-side)
|
|
60
|
+
*/
|
|
61
|
+
export async function getLanguages(
|
|
62
|
+
config: Omit<I18nCoreConfig, "defaultLocale"> & { defaultLocale?: string },
|
|
63
|
+
): Promise<LanguageOption[]> {
|
|
64
|
+
const i18n = createI18nCore({
|
|
65
|
+
project: config.project,
|
|
66
|
+
defaultLocale: config.defaultLocale || "en",
|
|
67
|
+
cdnBaseUrl: config.cdnBaseUrl,
|
|
68
|
+
debug: config.debug,
|
|
69
|
+
logLevel: config.logLevel,
|
|
70
|
+
fetch: config.fetch,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return i18n.getLanguages();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a translator function for use outside React (server-side)
|
|
78
|
+
*/
|
|
79
|
+
export function createServerTranslator(config: {
|
|
80
|
+
locale: string;
|
|
81
|
+
messages: Messages;
|
|
82
|
+
namespace?: string;
|
|
83
|
+
}) {
|
|
84
|
+
return createTranslator({
|
|
85
|
+
locale: config.locale,
|
|
86
|
+
messages: config.messages as Parameters<
|
|
87
|
+
typeof createTranslator
|
|
88
|
+
>[0]["messages"],
|
|
89
|
+
namespace: config.namespace,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create a formatter for use outside React (server-side)
|
|
95
|
+
*/
|
|
96
|
+
export function createServerFormatter(config: {
|
|
97
|
+
locale: string;
|
|
98
|
+
timeZone?: string;
|
|
99
|
+
now?: Date;
|
|
100
|
+
}) {
|
|
101
|
+
return createFormatter({
|
|
102
|
+
locale: config.locale,
|
|
103
|
+
timeZone: config.timeZone,
|
|
104
|
+
now: config.now,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { createTranslator, createFormatter } from "use-intl/core";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { I18nCoreConfig, LanguageOption } from "@better-i18n/core";
|
|
2
|
+
import type { ComponentProps } from "react";
|
|
3
|
+
import type { IntlProvider } from "use-intl";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Messages type (compatible with use-intl)
|
|
7
|
+
*/
|
|
8
|
+
export type Messages = ComponentProps<typeof IntlProvider>["messages"];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Configuration for BetterI18nProvider
|
|
12
|
+
*/
|
|
13
|
+
export interface BetterI18nProviderConfig
|
|
14
|
+
extends Omit<I18nCoreConfig, "defaultLocale"> {
|
|
15
|
+
/**
|
|
16
|
+
* Current locale
|
|
17
|
+
*/
|
|
18
|
+
locale: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Pre-loaded messages (for SSR hydration)
|
|
22
|
+
*/
|
|
23
|
+
messages?: Messages;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Timezone for date/time formatting
|
|
27
|
+
* @default undefined (uses browser timezone)
|
|
28
|
+
*/
|
|
29
|
+
timeZone?: string;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Current date/time (useful for SSR to prevent hydration mismatches)
|
|
33
|
+
*/
|
|
34
|
+
now?: Date;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Callback when locale changes
|
|
38
|
+
*/
|
|
39
|
+
onLocaleChange?: (locale: string) => void;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Error handler for missing translations
|
|
43
|
+
*/
|
|
44
|
+
onError?: ComponentProps<typeof IntlProvider>["onError"];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Better i18n context value
|
|
49
|
+
*/
|
|
50
|
+
export interface BetterI18nContextValue {
|
|
51
|
+
/**
|
|
52
|
+
* Current locale
|
|
53
|
+
*/
|
|
54
|
+
locale: string;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Change the current locale
|
|
58
|
+
*/
|
|
59
|
+
setLocale: (locale: string) => void;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Available languages with metadata
|
|
63
|
+
*/
|
|
64
|
+
languages: LanguageOption[];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Whether languages are still loading
|
|
68
|
+
*/
|
|
69
|
+
isLoadingLanguages: boolean;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether messages are still loading
|
|
73
|
+
*/
|
|
74
|
+
isLoadingMessages: boolean;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Project identifier
|
|
78
|
+
*/
|
|
79
|
+
project: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Server-side configuration for getMessages
|
|
84
|
+
*/
|
|
85
|
+
export interface GetMessagesConfig extends I18nCoreConfig {
|
|
86
|
+
/**
|
|
87
|
+
* Locale to fetch messages for
|
|
88
|
+
*/
|
|
89
|
+
locale: string;
|
|
90
|
+
}
|