@better-i18n/use-intl 0.1.6 → 0.1.8
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 +10 -3
- package/src/components.tsx +14 -14
- package/src/context.tsx +24 -8
- package/src/hooks/useLocaleRouter.ts +147 -0
- package/src/hooks.ts +22 -8
- package/src/index.ts +16 -0
- package/src/middleware/index.ts +66 -13
- package/src/provider.tsx +31 -44
- package/src/server.ts +1 -1
- package/src/types.ts +6 -13
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.8",
|
|
4
4
|
"description": "Better i18n integration for use-intl (React, TanStack Start)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -44,14 +44,21 @@
|
|
|
44
44
|
"clean": "rm -rf dist"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@better-i18n/core": "0.1.
|
|
47
|
+
"@better-i18n/core": "0.1.8"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"react": ">=18.0.0",
|
|
51
|
-
"use-intl": ">=4.0.0"
|
|
51
|
+
"use-intl": ">=4.0.0",
|
|
52
|
+
"@tanstack/react-router": ">=1.0.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"@tanstack/react-router": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
52
58
|
},
|
|
53
59
|
"devDependencies": {
|
|
54
60
|
"@types/react": "^19.0.0",
|
|
61
|
+
"@tanstack/react-router": "^1.120.3",
|
|
55
62
|
"react": "^19.0.0",
|
|
56
63
|
"use-intl": "^4.7.0",
|
|
57
64
|
"typescript": "~5.9.2"
|
package/src/components.tsx
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { ComponentProps, ReactNode } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { useLocaleRouter } from "./hooks/useLocaleRouter";
|
|
5
|
+
import { useLanguages } from "./hooks";
|
|
5
6
|
|
|
6
|
-
export interface LanguageSwitcherProps
|
|
7
|
-
ComponentProps<"select">,
|
|
8
|
-
"value" | "onChange" | "children"
|
|
9
|
-
> {
|
|
7
|
+
export interface LanguageSwitcherProps
|
|
8
|
+
extends Omit<ComponentProps<"select">, "value" | "onChange" | "children"> {
|
|
10
9
|
/**
|
|
11
10
|
* Render function for custom option display
|
|
12
11
|
*/
|
|
@@ -15,6 +14,7 @@ export interface LanguageSwitcherProps extends Omit<
|
|
|
15
14
|
name?: string;
|
|
16
15
|
nativeName?: string;
|
|
17
16
|
flagUrl?: string | null;
|
|
17
|
+
isDefault?: boolean;
|
|
18
18
|
}) => ReactNode;
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -24,11 +24,14 @@ export interface LanguageSwitcherProps extends Omit<
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Pre-built language switcher component
|
|
27
|
+
* Pre-built language switcher component with router integration
|
|
28
|
+
*
|
|
29
|
+
* Uses useLocaleRouter() internally for proper SPA navigation.
|
|
30
|
+
* Locale changes trigger router navigation, which re-executes loaders.
|
|
28
31
|
*
|
|
29
32
|
* @example
|
|
30
33
|
* ```tsx
|
|
31
|
-
* // Basic usage
|
|
34
|
+
* // Basic usage - just works!
|
|
32
35
|
* <LanguageSwitcher />
|
|
33
36
|
*
|
|
34
37
|
* // With custom styling
|
|
@@ -50,9 +53,10 @@ export function LanguageSwitcher({
|
|
|
50
53
|
loadingLabel = "Loading...",
|
|
51
54
|
...props
|
|
52
55
|
}: LanguageSwitcherProps) {
|
|
53
|
-
const { locale,
|
|
56
|
+
const { locale, navigate, isReady } = useLocaleRouter();
|
|
57
|
+
const { languages } = useLanguages();
|
|
54
58
|
|
|
55
|
-
if (
|
|
59
|
+
if (!isReady) {
|
|
56
60
|
return (
|
|
57
61
|
<select disabled {...props}>
|
|
58
62
|
<option>{loadingLabel}</option>
|
|
@@ -61,11 +65,7 @@ export function LanguageSwitcher({
|
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
return (
|
|
64
|
-
<select
|
|
65
|
-
value={locale}
|
|
66
|
-
onChange={(e) => setLocale(e.target.value)}
|
|
67
|
-
{...props}
|
|
68
|
-
>
|
|
68
|
+
<select value={locale} onChange={(e) => navigate(e.target.value)} {...props}>
|
|
69
69
|
{languages.map((lang) => (
|
|
70
70
|
<option key={lang.code} value={lang.code}>
|
|
71
71
|
{renderOption ? renderOption(lang) : lang.nativeName || lang.code}
|
package/src/context.tsx
CHANGED
|
@@ -13,18 +13,34 @@ export const BetterI18nContext = createContext<BetterI18nContextValue | null>(
|
|
|
13
13
|
/**
|
|
14
14
|
* Hook to access Better i18n context
|
|
15
15
|
*
|
|
16
|
+
* Note: For locale switching, use useLocaleRouter() which integrates with TanStack Router.
|
|
17
|
+
*
|
|
16
18
|
* @example
|
|
17
19
|
* ```tsx
|
|
18
|
-
* function
|
|
19
|
-
* const { locale,
|
|
20
|
+
* function LanguageInfo() {
|
|
21
|
+
* const { locale, languages, isLoadingLanguages } = useBetterI18n()
|
|
22
|
+
*
|
|
23
|
+
* if (isLoadingLanguages) return <div>Loading...</div>
|
|
24
|
+
*
|
|
25
|
+
* return (
|
|
26
|
+
* <div>
|
|
27
|
+
* Current: {locale}
|
|
28
|
+
* Available: {languages.map(l => l.code).join(', ')}
|
|
29
|
+
* </div>
|
|
30
|
+
* )
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* // For language switching with proper router navigation:
|
|
37
|
+
* import { useLocaleRouter } from '@better-i18n/use-intl'
|
|
20
38
|
*
|
|
39
|
+
* function LanguageSwitcher() {
|
|
40
|
+
* const { locale, locales, navigate } = useLocaleRouter()
|
|
21
41
|
* return (
|
|
22
|
-
* <select value={locale} onChange={(e) =>
|
|
23
|
-
* {
|
|
24
|
-
* <option key={lang.code} value={lang.code}>
|
|
25
|
-
* {lang.nativeName}
|
|
26
|
-
* </option>
|
|
27
|
-
* ))}
|
|
42
|
+
* <select value={locale} onChange={(e) => navigate(e.target.value)}>
|
|
43
|
+
* {locales.map((loc) => <option key={loc} value={loc}>{loc}</option>)}
|
|
28
44
|
* </select>
|
|
29
45
|
* )
|
|
30
46
|
* }
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo } from "react";
|
|
4
|
+
import { useRouter, useLocation } from "@tanstack/react-router";
|
|
5
|
+
import {
|
|
6
|
+
getLocaleFromPath,
|
|
7
|
+
replaceLocaleInPath,
|
|
8
|
+
addLocalePrefix,
|
|
9
|
+
type LocaleConfig,
|
|
10
|
+
} from "@better-i18n/core";
|
|
11
|
+
import { useBetterI18n } from "../context";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Return type for useLocaleRouter hook
|
|
15
|
+
*/
|
|
16
|
+
export interface UseLocaleRouterReturn {
|
|
17
|
+
/**
|
|
18
|
+
* Current locale (extracted from URL or default)
|
|
19
|
+
*/
|
|
20
|
+
locale: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Available locales from CDN manifest
|
|
24
|
+
*/
|
|
25
|
+
locales: string[];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Default locale (no URL prefix)
|
|
29
|
+
*/
|
|
30
|
+
defaultLocale: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Navigate to the same page with a new locale
|
|
34
|
+
* Uses TanStack Router's navigate() for proper SPA navigation
|
|
35
|
+
*/
|
|
36
|
+
navigate: (locale: string) => void;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get a localized path for link building
|
|
40
|
+
* @param path - The path to localize
|
|
41
|
+
* @param locale - Target locale (optional, uses current if not specified)
|
|
42
|
+
*/
|
|
43
|
+
localePath: (path: string, locale?: string) => string;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether languages have been loaded from CDN
|
|
47
|
+
*/
|
|
48
|
+
isReady: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Hook for router-integrated locale navigation
|
|
53
|
+
*
|
|
54
|
+
* This hook provides a navigation-first approach to locale switching:
|
|
55
|
+
* - Locale changes trigger proper router navigation
|
|
56
|
+
* - Loaders re-execute with the new locale
|
|
57
|
+
* - No state synchronization issues
|
|
58
|
+
* - Works with TanStack Router's file-based routing
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* function LanguageSwitcher() {
|
|
63
|
+
* const { locale, locales, navigate, isReady } = useLocaleRouter();
|
|
64
|
+
*
|
|
65
|
+
* if (!isReady) return <Skeleton />;
|
|
66
|
+
*
|
|
67
|
+
* return (
|
|
68
|
+
* <select value={locale} onChange={(e) => navigate(e.target.value)}>
|
|
69
|
+
* {locales.map((loc) => (
|
|
70
|
+
* <option key={loc} value={loc}>{loc}</option>
|
|
71
|
+
* ))}
|
|
72
|
+
* </select>
|
|
73
|
+
* );
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```tsx
|
|
79
|
+
* // Building localized links
|
|
80
|
+
* function Navigation() {
|
|
81
|
+
* const { localePath } = useLocaleRouter();
|
|
82
|
+
*
|
|
83
|
+
* return (
|
|
84
|
+
* <nav>
|
|
85
|
+
* <Link to={localePath('/about')}>About</Link>
|
|
86
|
+
* <Link to={localePath('/contact', 'tr')}>İletişim (TR)</Link>
|
|
87
|
+
* </nav>
|
|
88
|
+
* );
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export function useLocaleRouter(): UseLocaleRouterReturn {
|
|
93
|
+
const router = useRouter();
|
|
94
|
+
const location = useLocation();
|
|
95
|
+
const { languages, isLoadingLanguages } = useBetterI18n();
|
|
96
|
+
|
|
97
|
+
// Build config from CDN manifest
|
|
98
|
+
const config: LocaleConfig = useMemo(
|
|
99
|
+
() => ({
|
|
100
|
+
locales: languages.map((lang) => lang.code),
|
|
101
|
+
defaultLocale: languages.find((l) => l.isDefault)?.code || "en",
|
|
102
|
+
}),
|
|
103
|
+
[languages]
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Get effective locale from URL (handles default without prefix)
|
|
107
|
+
const locale = useMemo(() => {
|
|
108
|
+
// If no languages loaded yet, extract from path manually
|
|
109
|
+
if (languages.length === 0) {
|
|
110
|
+
const segments = location.pathname.split("/").filter(Boolean);
|
|
111
|
+
const firstSegment = segments[0];
|
|
112
|
+
// Check if it looks like a locale code (2 letters)
|
|
113
|
+
if (firstSegment && /^[a-z]{2}$/i.test(firstSegment)) {
|
|
114
|
+
return firstSegment;
|
|
115
|
+
}
|
|
116
|
+
return "en"; // fallback
|
|
117
|
+
}
|
|
118
|
+
return getLocaleFromPath(location.pathname, config);
|
|
119
|
+
}, [location.pathname, config, languages]);
|
|
120
|
+
|
|
121
|
+
// Navigate to same page with new locale (SPA navigation!)
|
|
122
|
+
const navigate = useCallback(
|
|
123
|
+
(newLocale: string) => {
|
|
124
|
+
const newPath = replaceLocaleInPath(location.pathname, newLocale, config);
|
|
125
|
+
router.navigate({ to: newPath });
|
|
126
|
+
},
|
|
127
|
+
[location.pathname, config, router]
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Get localized path for links
|
|
131
|
+
const localePath = useCallback(
|
|
132
|
+
(path: string, targetLocale?: string) => {
|
|
133
|
+
const loc = targetLocale || locale;
|
|
134
|
+
return addLocalePrefix(path, loc, config);
|
|
135
|
+
},
|
|
136
|
+
[locale, config]
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
locale,
|
|
141
|
+
locales: config.locales,
|
|
142
|
+
defaultLocale: config.defaultLocale,
|
|
143
|
+
navigate,
|
|
144
|
+
localePath,
|
|
145
|
+
isReady: !isLoadingLanguages && config.locales.length > 0,
|
|
146
|
+
};
|
|
147
|
+
}
|
package/src/hooks.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useBetterI18n } from "./context";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Hook to get available languages
|
|
6
|
+
* Hook to get available languages from CDN manifest
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
9
9
|
* ```tsx
|
|
@@ -34,27 +34,41 @@ export function useLanguages() {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Hook to get
|
|
37
|
+
* Hook to get current locale (read-only)
|
|
38
|
+
*
|
|
39
|
+
* For locale switching with proper router navigation, use useLocaleRouter() instead.
|
|
38
40
|
*
|
|
39
41
|
* @example
|
|
40
42
|
* ```tsx
|
|
41
|
-
* function
|
|
42
|
-
* const { locale,
|
|
43
|
+
* function LocaleDisplay() {
|
|
44
|
+
* const { locale, isLoading } = useLocale()
|
|
45
|
+
*
|
|
46
|
+
* if (isLoading) return <div>Loading...</div>
|
|
43
47
|
*
|
|
48
|
+
* return <span>Current locale: {locale}</span>
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* // For locale switching, use useLocaleRouter:
|
|
55
|
+
* import { useLocaleRouter } from '@better-i18n/use-intl'
|
|
56
|
+
*
|
|
57
|
+
* function LocaleSwitcher() {
|
|
58
|
+
* const { locale, navigate } = useLocaleRouter()
|
|
44
59
|
* return (
|
|
45
|
-
* <button onClick={() =>
|
|
46
|
-
*
|
|
60
|
+
* <button onClick={() => navigate(locale === 'en' ? 'tr' : 'en')}>
|
|
61
|
+
* Toggle: {locale}
|
|
47
62
|
* </button>
|
|
48
63
|
* )
|
|
49
64
|
* }
|
|
50
65
|
* ```
|
|
51
66
|
*/
|
|
52
67
|
export function useLocale() {
|
|
53
|
-
const { locale,
|
|
68
|
+
const { locale, isLoadingMessages } = useBetterI18n();
|
|
54
69
|
|
|
55
70
|
return {
|
|
56
71
|
locale,
|
|
57
|
-
setLocale,
|
|
58
72
|
isLoading: isLoadingMessages,
|
|
59
73
|
};
|
|
60
74
|
}
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,10 @@ export {
|
|
|
15
15
|
useTimeZone,
|
|
16
16
|
} from "./hooks";
|
|
17
17
|
|
|
18
|
+
// Router Integration (TanStack Router)
|
|
19
|
+
export { useLocaleRouter } from "./hooks/useLocaleRouter";
|
|
20
|
+
export type { UseLocaleRouterReturn } from "./hooks/useLocaleRouter";
|
|
21
|
+
|
|
18
22
|
// Components
|
|
19
23
|
export { LanguageSwitcher } from "./components";
|
|
20
24
|
export type { LanguageSwitcherProps } from "./components";
|
|
@@ -26,5 +30,17 @@ export type {
|
|
|
26
30
|
BetterI18nContextValue,
|
|
27
31
|
} from "./types";
|
|
28
32
|
|
|
33
|
+
// Re-export locale utilities from core (convenience)
|
|
34
|
+
export {
|
|
35
|
+
extractLocale,
|
|
36
|
+
getLocaleFromPath,
|
|
37
|
+
hasLocalePrefix,
|
|
38
|
+
removeLocalePrefix,
|
|
39
|
+
addLocalePrefix,
|
|
40
|
+
replaceLocaleInPath,
|
|
41
|
+
createLocalePath,
|
|
42
|
+
type LocaleConfig,
|
|
43
|
+
} from "@better-i18n/core";
|
|
44
|
+
|
|
29
45
|
// Re-export commonly used use-intl components
|
|
30
46
|
export { IntlProvider } from "use-intl";
|
package/src/middleware/index.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
// @ts-
|
|
1
|
+
// @ts-expect-error - internal workspace dependency
|
|
2
2
|
import { createMiddleware } from "@tanstack/react-router";
|
|
3
|
-
// @ts-
|
|
3
|
+
// @ts-expect-error - internal workspace dependency
|
|
4
4
|
import { detectLocale, getLocales } from "@better-i18n/core";
|
|
5
|
-
// @ts-ignore - internal workspace dependency
|
|
6
5
|
import type { I18nMiddlewareConfig } from "@better-i18n/core";
|
|
7
6
|
|
|
8
7
|
export function createBetterI18nMiddleware(config: I18nMiddlewareConfig) {
|
|
9
|
-
const { project, defaultLocale, detection = {} } = config;
|
|
8
|
+
const { project, defaultLocale, localePrefix = "as-needed", detection = {} } = config;
|
|
10
9
|
|
|
11
10
|
const {
|
|
12
11
|
cookie = true,
|
|
@@ -16,19 +15,37 @@ export function createBetterI18nMiddleware(config: I18nMiddlewareConfig) {
|
|
|
16
15
|
} = detection;
|
|
17
16
|
|
|
18
17
|
return createMiddleware().server(
|
|
19
|
-
async ({
|
|
18
|
+
async ({
|
|
19
|
+
next,
|
|
20
|
+
request,
|
|
21
|
+
}: {
|
|
22
|
+
next: (ctx: { context: { locale: string } }) => Promise<unknown>;
|
|
23
|
+
request: Request;
|
|
24
|
+
}) => {
|
|
20
25
|
// 1. Fetch available locales from CDN (cached)
|
|
21
26
|
const availableLocales = await getLocales({ project });
|
|
22
27
|
|
|
23
28
|
// 2. Extract locale indicators
|
|
24
29
|
const url = new URL(request.url);
|
|
25
|
-
const
|
|
30
|
+
const pathSegment = url.pathname.split("/")[1];
|
|
31
|
+
const hasLocaleInPath =
|
|
32
|
+
!!pathSegment && availableLocales.includes(pathSegment);
|
|
26
33
|
|
|
27
34
|
// Dynamic imports for TanStack Start server functions to avoid bundling them in client
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
const {
|
|
36
|
+
getCookie,
|
|
37
|
+
setCookie,
|
|
38
|
+
getRequestHeader,
|
|
39
|
+
}: {
|
|
40
|
+
getCookie: (name: string) => string | null;
|
|
41
|
+
setCookie: (
|
|
42
|
+
name: string,
|
|
43
|
+
value: string,
|
|
44
|
+
options: { path: string; maxAge: number; sameSite: string },
|
|
45
|
+
) => void;
|
|
46
|
+
getRequestHeader: (name: string) => string | undefined;
|
|
47
|
+
// @ts-expect-error - optional runtime dependency
|
|
48
|
+
} = await import("@tanstack/react-start/server");
|
|
32
49
|
|
|
33
50
|
const cookieLocale = cookie ? getCookie(cookieName) : null;
|
|
34
51
|
const headerLocale = browserLanguage
|
|
@@ -39,13 +56,49 @@ export function createBetterI18nMiddleware(config: I18nMiddlewareConfig) {
|
|
|
39
56
|
const result = detectLocale({
|
|
40
57
|
project,
|
|
41
58
|
defaultLocale,
|
|
42
|
-
pathLocale,
|
|
59
|
+
pathLocale: pathSegment,
|
|
43
60
|
cookieLocale,
|
|
44
61
|
headerLocale,
|
|
45
62
|
availableLocales,
|
|
46
63
|
});
|
|
47
64
|
|
|
48
|
-
// 4.
|
|
65
|
+
// 4. Redirect if locale prefix is needed but missing
|
|
66
|
+
// Skip API routes and paths that already have a locale prefix
|
|
67
|
+
const isApiRoute = url.pathname.startsWith("/api/");
|
|
68
|
+
if (
|
|
69
|
+
localePrefix !== "never" &&
|
|
70
|
+
!hasLocaleInPath &&
|
|
71
|
+
!isApiRoute &&
|
|
72
|
+
result.detectedFrom !== "path"
|
|
73
|
+
) {
|
|
74
|
+
const shouldRedirect =
|
|
75
|
+
localePrefix === "always" ||
|
|
76
|
+
(localePrefix === "as-needed" && result.locale !== defaultLocale);
|
|
77
|
+
|
|
78
|
+
if (shouldRedirect) {
|
|
79
|
+
const redirectUrl = new URL(
|
|
80
|
+
`/${result.locale}${url.pathname}`,
|
|
81
|
+
url.origin,
|
|
82
|
+
);
|
|
83
|
+
redirectUrl.search = url.search;
|
|
84
|
+
|
|
85
|
+
// Build redirect response with locale cookie
|
|
86
|
+
const headers = new Headers({
|
|
87
|
+
Location: redirectUrl.toString(),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (cookie && result.shouldSetCookie) {
|
|
91
|
+
headers.set(
|
|
92
|
+
"Set-Cookie",
|
|
93
|
+
`${cookieName}=${result.locale}; Path=/; Max-Age=${cookieMaxAge}; SameSite=Lax`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return new Response(null, { status: 302, headers });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 5. Set cookie if needed (non-redirect path)
|
|
49
102
|
if (cookie && result.shouldSetCookie) {
|
|
50
103
|
setCookie(cookieName, result.locale, {
|
|
51
104
|
path: "/",
|
|
@@ -54,7 +107,7 @@ export function createBetterI18nMiddleware(config: I18nMiddlewareConfig) {
|
|
|
54
107
|
});
|
|
55
108
|
}
|
|
56
109
|
|
|
57
|
-
//
|
|
110
|
+
// 6. Pass locale to route context
|
|
58
111
|
return next({ context: { locale: result.locale } });
|
|
59
112
|
},
|
|
60
113
|
);
|
package/src/provider.tsx
CHANGED
|
@@ -2,13 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { createI18nCore } from "@better-i18n/core";
|
|
4
4
|
import type { LanguageOption } from "@better-i18n/core";
|
|
5
|
-
import {
|
|
6
|
-
useCallback,
|
|
7
|
-
useEffect,
|
|
8
|
-
useMemo,
|
|
9
|
-
useState,
|
|
10
|
-
type ReactNode,
|
|
11
|
-
} from "react";
|
|
5
|
+
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
|
12
6
|
import { IntlProvider } from "use-intl";
|
|
13
7
|
import { BetterI18nContext } from "./context.js";
|
|
14
8
|
import type { BetterI18nProviderConfig, Messages } from "./types.js";
|
|
@@ -20,6 +14,9 @@ export interface BetterI18nProviderProps extends BetterI18nProviderConfig {
|
|
|
20
14
|
/**
|
|
21
15
|
* Provider component that combines Better i18n CDN with use-intl
|
|
22
16
|
*
|
|
17
|
+
* The locale is controlled externally (from URL/router). Use useLocaleRouter()
|
|
18
|
+
* for locale switching with proper router integration.
|
|
19
|
+
*
|
|
23
20
|
* @example
|
|
24
21
|
* ```tsx
|
|
25
22
|
* // Basic usage (CSR - fetches messages on client)
|
|
@@ -34,15 +31,16 @@ export interface BetterI18nProviderProps extends BetterI18nProviderConfig {
|
|
|
34
31
|
* )
|
|
35
32
|
* }
|
|
36
33
|
*
|
|
37
|
-
* // SSR usage (pre-loaded messages)
|
|
38
|
-
* function
|
|
34
|
+
* // TanStack Router SSR usage (pre-loaded messages from loader)
|
|
35
|
+
* function RootComponent() {
|
|
36
|
+
* const { messages, locale } = Route.useLoaderData()
|
|
39
37
|
* return (
|
|
40
38
|
* <BetterI18nProvider
|
|
41
39
|
* project="acme/dashboard"
|
|
42
40
|
* locale={locale}
|
|
43
41
|
* messages={messages}
|
|
44
42
|
* >
|
|
45
|
-
* <
|
|
43
|
+
* <Outlet />
|
|
46
44
|
* </BetterI18nProvider>
|
|
47
45
|
* )
|
|
48
46
|
* }
|
|
@@ -51,37 +49,43 @@ export interface BetterI18nProviderProps extends BetterI18nProviderConfig {
|
|
|
51
49
|
export function BetterI18nProvider({
|
|
52
50
|
children,
|
|
53
51
|
project,
|
|
54
|
-
locale:
|
|
55
|
-
messages:
|
|
52
|
+
locale: propLocale,
|
|
53
|
+
messages: propMessages,
|
|
56
54
|
timeZone,
|
|
57
55
|
now,
|
|
58
|
-
onLocaleChange,
|
|
59
56
|
onError,
|
|
60
57
|
cdnBaseUrl,
|
|
61
58
|
debug,
|
|
62
59
|
logLevel,
|
|
63
60
|
fetch: customFetch,
|
|
64
61
|
}: BetterI18nProviderProps) {
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
);
|
|
62
|
+
// Locale is controlled by props (from URL/router)
|
|
63
|
+
const locale = propLocale;
|
|
64
|
+
const [messages, setMessages] = useState<Messages | undefined>(propMessages);
|
|
69
65
|
const [languages, setLanguages] = useState<LanguageOption[]>([]);
|
|
70
|
-
const [isLoadingMessages, setIsLoadingMessages] = useState(!
|
|
66
|
+
const [isLoadingMessages, setIsLoadingMessages] = useState(!propMessages);
|
|
71
67
|
const [isLoadingLanguages, setIsLoadingLanguages] = useState(true);
|
|
72
68
|
|
|
69
|
+
// Sync messages when props change (e.g., from router navigation with new loader data)
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (propMessages) {
|
|
72
|
+
setMessages(propMessages);
|
|
73
|
+
setIsLoadingMessages(false);
|
|
74
|
+
}
|
|
75
|
+
}, [propMessages]);
|
|
76
|
+
|
|
73
77
|
// Create i18n core instance
|
|
74
78
|
const i18nCore = useMemo(
|
|
75
79
|
() =>
|
|
76
80
|
createI18nCore({
|
|
77
81
|
project,
|
|
78
|
-
defaultLocale:
|
|
82
|
+
defaultLocale: locale,
|
|
79
83
|
cdnBaseUrl,
|
|
80
84
|
debug,
|
|
81
85
|
logLevel,
|
|
82
86
|
fetch: customFetch,
|
|
83
87
|
}),
|
|
84
|
-
[project,
|
|
88
|
+
[project, locale, cdnBaseUrl, debug, logLevel, customFetch]
|
|
85
89
|
);
|
|
86
90
|
|
|
87
91
|
// Load languages on mount
|
|
@@ -110,10 +114,10 @@ export function BetterI18nProvider({
|
|
|
110
114
|
};
|
|
111
115
|
}, [i18nCore]);
|
|
112
116
|
|
|
113
|
-
// Load messages when locale changes
|
|
117
|
+
// Load messages when locale changes and no pre-loaded messages available
|
|
114
118
|
useEffect(() => {
|
|
115
|
-
// Skip if we have
|
|
116
|
-
if (
|
|
119
|
+
// Skip if we already have messages for this render
|
|
120
|
+
if (propMessages) {
|
|
117
121
|
return;
|
|
118
122
|
}
|
|
119
123
|
|
|
@@ -130,7 +134,7 @@ export function BetterI18nProvider({
|
|
|
130
134
|
} catch (error) {
|
|
131
135
|
console.error(
|
|
132
136
|
`[better-i18n] Failed to load messages for locale "${locale}":`,
|
|
133
|
-
error
|
|
137
|
+
error
|
|
134
138
|
);
|
|
135
139
|
} finally {
|
|
136
140
|
if (!cancelled) {
|
|
@@ -144,35 +148,18 @@ export function BetterI18nProvider({
|
|
|
144
148
|
return () => {
|
|
145
149
|
cancelled = true;
|
|
146
150
|
};
|
|
147
|
-
}, [locale, i18nCore,
|
|
148
|
-
|
|
149
|
-
// Locale change handler
|
|
150
|
-
const setLocale = useCallback(
|
|
151
|
-
(newLocale: string) => {
|
|
152
|
-
setLocaleState(newLocale);
|
|
153
|
-
onLocaleChange?.(newLocale);
|
|
154
|
-
},
|
|
155
|
-
[onLocaleChange],
|
|
156
|
-
);
|
|
151
|
+
}, [locale, i18nCore, propMessages]);
|
|
157
152
|
|
|
158
|
-
// Context value
|
|
153
|
+
// Context value (read-only locale - use useLocaleRouter for navigation)
|
|
159
154
|
const contextValue = useMemo(
|
|
160
155
|
() => ({
|
|
161
156
|
locale,
|
|
162
|
-
setLocale,
|
|
163
157
|
languages,
|
|
164
158
|
isLoadingLanguages,
|
|
165
159
|
isLoadingMessages,
|
|
166
160
|
project,
|
|
167
161
|
}),
|
|
168
|
-
[
|
|
169
|
-
locale,
|
|
170
|
-
setLocale,
|
|
171
|
-
languages,
|
|
172
|
-
isLoadingLanguages,
|
|
173
|
-
isLoadingMessages,
|
|
174
|
-
project,
|
|
175
|
-
],
|
|
162
|
+
[locale, languages, isLoadingLanguages, isLoadingMessages, project]
|
|
176
163
|
);
|
|
177
164
|
|
|
178
165
|
// Don't render until we have messages
|
package/src/server.ts
CHANGED
|
@@ -28,7 +28,7 @@ export async function getMessages(
|
|
|
28
28
|
fetch: config.fetch,
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
const messages = (await i18n.getMessages(config.locale)) as
|
|
31
|
+
const messages = (await i18n.getMessages(config.locale)) as Messages;
|
|
32
32
|
|
|
33
33
|
// better-i18n convention: JSON matches exact namespace structure.
|
|
34
34
|
// if CDN returns { "hero": { "title": "..." } }, use-intl expects exactly that
|
package/src/types.ts
CHANGED
|
@@ -33,11 +33,6 @@ export interface BetterI18nProviderConfig
|
|
|
33
33
|
*/
|
|
34
34
|
now?: Date;
|
|
35
35
|
|
|
36
|
-
/**
|
|
37
|
-
* Callback when locale changes
|
|
38
|
-
*/
|
|
39
|
-
onLocaleChange?: (locale: string) => void;
|
|
40
|
-
|
|
41
36
|
/**
|
|
42
37
|
* Error handler for missing translations
|
|
43
38
|
*/
|
|
@@ -46,25 +41,23 @@ export interface BetterI18nProviderConfig
|
|
|
46
41
|
|
|
47
42
|
/**
|
|
48
43
|
* Better i18n context value
|
|
44
|
+
*
|
|
45
|
+
* Note: Locale is read-only. Use useLocaleRouter().navigate() for locale changes
|
|
46
|
+
* to ensure proper router integration.
|
|
49
47
|
*/
|
|
50
48
|
export interface BetterI18nContextValue {
|
|
51
49
|
/**
|
|
52
|
-
* Current locale
|
|
50
|
+
* Current locale (read-only - use useLocaleRouter().navigate() to change)
|
|
53
51
|
*/
|
|
54
52
|
locale: string;
|
|
55
53
|
|
|
56
54
|
/**
|
|
57
|
-
*
|
|
58
|
-
*/
|
|
59
|
-
setLocale: (locale: string) => void;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Available languages with metadata
|
|
55
|
+
* Available languages with metadata from CDN manifest
|
|
63
56
|
*/
|
|
64
57
|
languages: LanguageOption[];
|
|
65
58
|
|
|
66
59
|
/**
|
|
67
|
-
* Whether languages are still loading
|
|
60
|
+
* Whether languages are still loading from CDN
|
|
68
61
|
*/
|
|
69
62
|
isLoadingLanguages: boolean;
|
|
70
63
|
|