@better-i18n/use-intl 0.1.8 → 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.
Files changed (53) hide show
  1. package/README.md +7 -0
  2. package/dist/__tests__/provider-config.test.d.ts +2 -0
  3. package/dist/__tests__/provider-config.test.d.ts.map +1 -0
  4. package/dist/__tests__/provider-config.test.js +79 -0
  5. package/dist/__tests__/provider-config.test.js.map +1 -0
  6. package/dist/components/locale-dropdown.d.ts +62 -0
  7. package/dist/components/locale-dropdown.d.ts.map +1 -0
  8. package/dist/components/locale-dropdown.js +298 -0
  9. package/dist/components/locale-dropdown.js.map +1 -0
  10. package/dist/components.d.ts +44 -0
  11. package/dist/components.d.ts.map +1 -0
  12. package/dist/components.js +38 -0
  13. package/dist/components.js.map +1 -0
  14. package/dist/context.d.ts +43 -0
  15. package/dist/context.d.ts.map +1 -0
  16. package/{src/context.tsx → dist/context.js} +8 -17
  17. package/dist/context.js.map +1 -0
  18. package/dist/hooks/useLocaleRouter.d.ts +75 -0
  19. package/dist/hooks/useLocaleRouter.d.ts.map +1 -0
  20. package/dist/hooks/useLocaleRouter.js +89 -0
  21. package/dist/hooks/useLocaleRouter.js.map +1 -0
  22. package/dist/hooks.d.ts +63 -0
  23. package/dist/hooks.d.ts.map +1 -0
  24. package/{src/hooks.ts → dist/hooks.js} +13 -25
  25. package/dist/hooks.js.map +1 -0
  26. package/dist/index.d.ts +14 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +17 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/middleware/index.d.ts +3 -0
  31. package/dist/middleware/index.d.ts.map +1 -0
  32. package/dist/middleware/index.js +68 -0
  33. package/dist/middleware/index.js.map +1 -0
  34. package/dist/provider.d.ts +51 -0
  35. package/dist/provider.d.ts.map +1 -0
  36. package/dist/provider.js +138 -0
  37. package/dist/provider.js.map +1 -0
  38. package/dist/server.d.ts +79 -0
  39. package/dist/server.d.ts.map +1 -0
  40. package/dist/server.js +156 -0
  41. package/dist/server.js.map +1 -0
  42. package/dist/types.d.ts +71 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +2 -0
  45. package/dist/types.js.map +1 -0
  46. package/package.json +22 -9
  47. package/src/components.tsx +0 -76
  48. package/src/hooks/useLocaleRouter.ts +0 -147
  49. package/src/index.ts +0 -46
  50. package/src/middleware/index.ts +0 -114
  51. package/src/provider.tsx +0 -183
  52. package/src/server.ts +0 -108
  53. package/src/types.ts +0 -83
@@ -1,147 +0,0 @@
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/index.ts DELETED
@@ -1,46 +0,0 @@
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
- // Router Integration (TanStack Router)
19
- export { useLocaleRouter } from "./hooks/useLocaleRouter";
20
- export type { UseLocaleRouterReturn } from "./hooks/useLocaleRouter";
21
-
22
- // Components
23
- export { LanguageSwitcher } from "./components";
24
- export type { LanguageSwitcherProps } from "./components";
25
-
26
- // Types
27
- export type {
28
- Messages,
29
- BetterI18nProviderConfig,
30
- BetterI18nContextValue,
31
- } from "./types";
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
-
45
- // Re-export commonly used use-intl components
46
- export { IntlProvider } from "use-intl";
@@ -1,114 +0,0 @@
1
- // @ts-expect-error - internal workspace dependency
2
- import { createMiddleware } from "@tanstack/react-router";
3
- // @ts-expect-error - internal workspace dependency
4
- import { detectLocale, getLocales } from "@better-i18n/core";
5
- import type { I18nMiddlewareConfig } from "@better-i18n/core";
6
-
7
- export function createBetterI18nMiddleware(config: I18nMiddlewareConfig) {
8
- const { project, defaultLocale, localePrefix = "as-needed", detection = {} } = config;
9
-
10
- const {
11
- cookie = true,
12
- browserLanguage = true,
13
- cookieName = "locale",
14
- cookieMaxAge = 31536000,
15
- } = detection;
16
-
17
- return createMiddleware().server(
18
- async ({
19
- next,
20
- request,
21
- }: {
22
- next: (ctx: { context: { locale: string } }) => Promise<unknown>;
23
- request: Request;
24
- }) => {
25
- // 1. Fetch available locales from CDN (cached)
26
- const availableLocales = await getLocales({ project });
27
-
28
- // 2. Extract locale indicators
29
- const url = new URL(request.url);
30
- const pathSegment = url.pathname.split("/")[1];
31
- const hasLocaleInPath =
32
- !!pathSegment && availableLocales.includes(pathSegment);
33
-
34
- // Dynamic imports for TanStack Start server functions to avoid bundling them in client
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");
49
-
50
- const cookieLocale = cookie ? getCookie(cookieName) : null;
51
- const headerLocale = browserLanguage
52
- ? getRequestHeader("accept-language")?.split(",")[0]?.split("-")[0]
53
- : null;
54
-
55
- // 3. Detect locale using core logic
56
- const result = detectLocale({
57
- project,
58
- defaultLocale,
59
- pathLocale: pathSegment,
60
- cookieLocale,
61
- headerLocale,
62
- availableLocales,
63
- });
64
-
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)
102
- if (cookie && result.shouldSetCookie) {
103
- setCookie(cookieName, result.locale, {
104
- path: "/",
105
- maxAge: cookieMaxAge,
106
- sameSite: "lax",
107
- });
108
- }
109
-
110
- // 6. Pass locale to route context
111
- return next({ context: { locale: result.locale } });
112
- },
113
- );
114
- }
package/src/provider.tsx DELETED
@@ -1,183 +0,0 @@
1
- "use client";
2
-
3
- import { createI18nCore } from "@better-i18n/core";
4
- import type { LanguageOption } from "@better-i18n/core";
5
- import { useEffect, useMemo, useState, type ReactNode } from "react";
6
- import { IntlProvider } from "use-intl";
7
- import { BetterI18nContext } from "./context.js";
8
- import type { BetterI18nProviderConfig, Messages } from "./types.js";
9
-
10
- export interface BetterI18nProviderProps extends BetterI18nProviderConfig {
11
- children: ReactNode;
12
- }
13
-
14
- /**
15
- * Provider component that combines Better i18n CDN with use-intl
16
- *
17
- * The locale is controlled externally (from URL/router). Use useLocaleRouter()
18
- * for locale switching with proper router integration.
19
- *
20
- * @example
21
- * ```tsx
22
- * // Basic usage (CSR - fetches messages on client)
23
- * function App() {
24
- * return (
25
- * <BetterI18nProvider
26
- * project="acme/dashboard"
27
- * locale="en"
28
- * >
29
- * <MyComponent />
30
- * </BetterI18nProvider>
31
- * )
32
- * }
33
- *
34
- * // TanStack Router SSR usage (pre-loaded messages from loader)
35
- * function RootComponent() {
36
- * const { messages, locale } = Route.useLoaderData()
37
- * return (
38
- * <BetterI18nProvider
39
- * project="acme/dashboard"
40
- * locale={locale}
41
- * messages={messages}
42
- * >
43
- * <Outlet />
44
- * </BetterI18nProvider>
45
- * )
46
- * }
47
- * ```
48
- */
49
- export function BetterI18nProvider({
50
- children,
51
- project,
52
- locale: propLocale,
53
- messages: propMessages,
54
- timeZone,
55
- now,
56
- onError,
57
- cdnBaseUrl,
58
- debug,
59
- logLevel,
60
- fetch: customFetch,
61
- }: BetterI18nProviderProps) {
62
- // Locale is controlled by props (from URL/router)
63
- const locale = propLocale;
64
- const [messages, setMessages] = useState<Messages | undefined>(propMessages);
65
- const [languages, setLanguages] = useState<LanguageOption[]>([]);
66
- const [isLoadingMessages, setIsLoadingMessages] = useState(!propMessages);
67
- const [isLoadingLanguages, setIsLoadingLanguages] = useState(true);
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
-
77
- // Create i18n core instance
78
- const i18nCore = useMemo(
79
- () =>
80
- createI18nCore({
81
- project,
82
- defaultLocale: locale,
83
- cdnBaseUrl,
84
- debug,
85
- logLevel,
86
- fetch: customFetch,
87
- }),
88
- [project, locale, cdnBaseUrl, debug, logLevel, customFetch]
89
- );
90
-
91
- // Load languages on mount
92
- useEffect(() => {
93
- let cancelled = false;
94
-
95
- const loadLanguages = async () => {
96
- try {
97
- const langs = await i18nCore.getLanguages();
98
- if (!cancelled) {
99
- setLanguages(langs);
100
- }
101
- } catch (error) {
102
- console.error("[better-i18n] Failed to load languages:", error);
103
- } finally {
104
- if (!cancelled) {
105
- setIsLoadingLanguages(false);
106
- }
107
- }
108
- };
109
-
110
- loadLanguages();
111
-
112
- return () => {
113
- cancelled = true;
114
- };
115
- }, [i18nCore]);
116
-
117
- // Load messages when locale changes and no pre-loaded messages available
118
- useEffect(() => {
119
- // Skip if we already have messages for this render
120
- if (propMessages) {
121
- return;
122
- }
123
-
124
- let cancelled = false;
125
-
126
- const loadMessages = async () => {
127
- setIsLoadingMessages(true);
128
-
129
- try {
130
- const msgs = await i18nCore.getMessages(locale);
131
- if (!cancelled) {
132
- setMessages(msgs as Messages);
133
- }
134
- } catch (error) {
135
- console.error(
136
- `[better-i18n] Failed to load messages for locale "${locale}":`,
137
- error
138
- );
139
- } finally {
140
- if (!cancelled) {
141
- setIsLoadingMessages(false);
142
- }
143
- }
144
- };
145
-
146
- loadMessages();
147
-
148
- return () => {
149
- cancelled = true;
150
- };
151
- }, [locale, i18nCore, propMessages]);
152
-
153
- // Context value (read-only locale - use useLocaleRouter for navigation)
154
- const contextValue = useMemo(
155
- () => ({
156
- locale,
157
- languages,
158
- isLoadingLanguages,
159
- isLoadingMessages,
160
- project,
161
- }),
162
- [locale, languages, isLoadingLanguages, isLoadingMessages, project]
163
- );
164
-
165
- // Don't render until we have messages
166
- if (!messages) {
167
- return null;
168
- }
169
-
170
- return (
171
- <BetterI18nContext.Provider value={contextValue}>
172
- <IntlProvider
173
- locale={locale}
174
- messages={messages}
175
- timeZone={timeZone}
176
- now={now}
177
- onError={onError}
178
- >
179
- {children}
180
- </IntlProvider>
181
- </BetterI18nContext.Provider>
182
- );
183
- }
package/src/server.ts DELETED
@@ -1,108 +0,0 @@
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 Messages;
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 DELETED
@@ -1,83 +0,0 @@
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
- * Error handler for missing translations
38
- */
39
- onError?: ComponentProps<typeof IntlProvider>["onError"];
40
- }
41
-
42
- /**
43
- * Better i18n context value
44
- *
45
- * Note: Locale is read-only. Use useLocaleRouter().navigate() for locale changes
46
- * to ensure proper router integration.
47
- */
48
- export interface BetterI18nContextValue {
49
- /**
50
- * Current locale (read-only - use useLocaleRouter().navigate() to change)
51
- */
52
- locale: string;
53
-
54
- /**
55
- * Available languages with metadata from CDN manifest
56
- */
57
- languages: LanguageOption[];
58
-
59
- /**
60
- * Whether languages are still loading from CDN
61
- */
62
- isLoadingLanguages: boolean;
63
-
64
- /**
65
- * Whether messages are still loading
66
- */
67
- isLoadingMessages: boolean;
68
-
69
- /**
70
- * Project identifier
71
- */
72
- project: string;
73
- }
74
-
75
- /**
76
- * Server-side configuration for getMessages
77
- */
78
- export interface GetMessagesConfig extends I18nCoreConfig {
79
- /**
80
- * Locale to fetch messages for
81
- */
82
- locale: string;
83
- }