@better-i18n/use-intl 0.1.3 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-i18n/use-intl",
3
- "version": "0.1.3",
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
- "dist",
25
+ "src",
26
26
  "package.json",
27
27
  "README.md"
28
28
  ],
@@ -44,7 +44,7 @@
44
44
  "clean": "rm -rf dist"
45
45
  },
46
46
  "dependencies": {
47
- "@better-i18n/core": "0.1.3"
47
+ "@better-i18n/core": "0.1.4"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "react": ">=18.0.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
+ }
@@ -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
+ }
@@ -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
+ }