@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
@@ -0,0 +1,138 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createI18nCore } from "@better-i18n/core";
4
+ import { useEffect, useMemo, useState } from "react";
5
+ import { IntlProvider } from "use-intl";
6
+ import { BetterI18nContext } from "./context.js";
7
+ /**
8
+ * Provider component that combines Better i18n CDN with use-intl
9
+ *
10
+ * The locale is controlled externally (from URL/router). Use useLocaleRouter()
11
+ * for locale switching with proper router integration.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * // Basic usage (CSR - fetches messages on client)
16
+ * function App() {
17
+ * return (
18
+ * <BetterI18nProvider
19
+ * project="acme/dashboard"
20
+ * locale="en"
21
+ * >
22
+ * <MyComponent />
23
+ * </BetterI18nProvider>
24
+ * )
25
+ * }
26
+ *
27
+ * // TanStack Router SSR usage (pre-loaded messages from loader)
28
+ * function RootComponent() {
29
+ * const { messages, locale } = Route.useLoaderData()
30
+ * return (
31
+ * <BetterI18nProvider
32
+ * project="acme/dashboard"
33
+ * locale={locale}
34
+ * messages={messages}
35
+ * >
36
+ * <Outlet />
37
+ * </BetterI18nProvider>
38
+ * )
39
+ * }
40
+ * ```
41
+ */
42
+ export function BetterI18nProvider({ children, project, locale: propLocale, messages: propMessages, timeZone, now, onError, cdnBaseUrl, debug, logLevel, fetch: customFetch, storage, staticData, fetchTimeout, retryCount, initialLanguages, getMessageFallback: customGetMessageFallback, }) {
43
+ // Locale is controlled by props (from URL/router)
44
+ const locale = propLocale;
45
+ // clientMessages is only used in CSR mode (when propMessages is not provided).
46
+ // In SSG/SSR mode, propMessages comes from the loader and is used directly —
47
+ // synchronous derivation avoids the useEffect delay that causes white flash on hydration.
48
+ const [clientMessages, setClientMessages] = useState();
49
+ const messages = propMessages ?? clientMessages;
50
+ const [languages, setLanguages] = useState(initialLanguages ?? []);
51
+ const [isLoadingMessages, setIsLoadingMessages] = useState(propMessages === undefined);
52
+ const [isLoadingLanguages, setIsLoadingLanguages] = useState(!initialLanguages);
53
+ // Create i18n core instance
54
+ const i18nCore = useMemo(() => createI18nCore({
55
+ project,
56
+ defaultLocale: locale,
57
+ cdnBaseUrl,
58
+ debug,
59
+ logLevel,
60
+ fetch: customFetch,
61
+ storage,
62
+ staticData,
63
+ fetchTimeout,
64
+ retryCount,
65
+ }), [project, locale, cdnBaseUrl, debug, logLevel, customFetch, storage, staticData, fetchTimeout, retryCount]);
66
+ // Load languages on mount — skip if SSR already provided them
67
+ useEffect(() => {
68
+ if (initialLanguages)
69
+ return;
70
+ let cancelled = false;
71
+ const loadLanguages = async () => {
72
+ try {
73
+ const langs = await i18nCore.getLanguages();
74
+ if (!cancelled) {
75
+ setLanguages(langs);
76
+ }
77
+ }
78
+ catch (error) {
79
+ console.error("[better-i18n] Failed to load languages:", error);
80
+ }
81
+ finally {
82
+ if (!cancelled) {
83
+ setIsLoadingLanguages(false);
84
+ }
85
+ }
86
+ };
87
+ loadLanguages();
88
+ return () => {
89
+ cancelled = true;
90
+ };
91
+ }, [i18nCore, initialLanguages]);
92
+ // Load messages when locale changes and no pre-loaded messages available
93
+ useEffect(() => {
94
+ // Skip if we already have messages for this render
95
+ if (propMessages) {
96
+ return;
97
+ }
98
+ let cancelled = false;
99
+ const loadMessages = async () => {
100
+ setIsLoadingMessages(true);
101
+ try {
102
+ const msgs = await i18nCore.getMessages(locale);
103
+ if (!cancelled) {
104
+ setClientMessages(msgs);
105
+ }
106
+ }
107
+ catch (error) {
108
+ console.error(`[better-i18n] Failed to load messages for locale "${locale}":`, error);
109
+ }
110
+ finally {
111
+ if (!cancelled) {
112
+ setIsLoadingMessages(false);
113
+ }
114
+ }
115
+ };
116
+ loadMessages();
117
+ return () => {
118
+ cancelled = true;
119
+ };
120
+ }, [locale, i18nCore, propMessages]);
121
+ // Context value (read-only locale - use useLocaleRouter for navigation)
122
+ const contextValue = useMemo(() => ({
123
+ locale,
124
+ languages,
125
+ isLoadingLanguages,
126
+ isLoadingMessages,
127
+ project,
128
+ }), [locale, languages, isLoadingLanguages, isLoadingMessages, project]);
129
+ if (!messages) {
130
+ // Render children with empty messages instead of blanking the screen.
131
+ // This converts a full DOM structural mismatch (null vs full tree)
132
+ // into a text content mismatch — React recovers silently and re-renders
133
+ // once the loader delivers real messages.
134
+ return (_jsx(BetterI18nContext.Provider, { value: contextValue, children: _jsx(IntlProvider, { locale: locale, messages: {}, timeZone: timeZone, onError: () => { }, getMessageFallback: customGetMessageFallback, children: children }) }));
135
+ }
136
+ return (_jsx(BetterI18nContext.Provider, { value: contextValue, children: _jsx(IntlProvider, { locale: locale, messages: messages, timeZone: timeZone, now: now, onError: onError ?? (() => { }), getMessageFallback: customGetMessageFallback, children: children }) }));
137
+ }
138
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ import type { I18nCoreConfig, LanguageOption } from "@better-i18n/core";
2
+ import { createFormatter } from "use-intl/core";
3
+ import type { Messages } from "./types.js";
4
+ export interface GetMessagesConfig extends Omit<I18nCoreConfig, "defaultLocale"> {
5
+ /**
6
+ * Locale to fetch messages for
7
+ */
8
+ locale: string;
9
+ }
10
+ /**
11
+ * Fetch messages for a locale (server-side)
12
+ */
13
+ export declare function getMessages(config: GetMessagesConfig): Promise<Messages>;
14
+ /**
15
+ * Fetch available locales (server-side)
16
+ */
17
+ export declare function getLocales(config: Omit<I18nCoreConfig, "defaultLocale"> & {
18
+ defaultLocale?: string;
19
+ }): Promise<string[]>;
20
+ /**
21
+ * Fetch available languages with metadata (server-side)
22
+ */
23
+ export declare function getLanguages(config: Omit<I18nCoreConfig, "defaultLocale"> & {
24
+ defaultLocale?: string;
25
+ }): Promise<LanguageOption[]>;
26
+ /**
27
+ * Create a translator function for use outside React (server-side)
28
+ */
29
+ export declare function createServerTranslator(config: {
30
+ locale: string;
31
+ messages: Messages;
32
+ namespace?: string;
33
+ }): import("use-intl")._Translator<{
34
+ [x: string]: any;
35
+ }, string>;
36
+ /**
37
+ * Create a formatter for use outside React (server-side)
38
+ */
39
+ export declare function createServerFormatter(config: {
40
+ locale: string;
41
+ timeZone?: string;
42
+ now?: Date;
43
+ }): ReturnType<typeof createFormatter>;
44
+ export { createTranslator, createFormatter } from "use-intl/core";
45
+ /**
46
+ * Parse an RFC 5646 Accept-Language header value into a prioritized list of language tags.
47
+ *
48
+ * @example
49
+ * parseAcceptLanguage("tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7")
50
+ * // → ["tr-TR", "tr", "en-US", "en"]
51
+ */
52
+ export declare function parseAcceptLanguage(header: string | null | undefined): string[];
53
+ /**
54
+ * Find the best matching locale from a parsed language list against available locales.
55
+ * Matching strategy (in order):
56
+ * 1. Exact match: "tr-TR" → "tr-TR"
57
+ * 2. Base language match: "tr-TR" → "tr"
58
+ * 3. Region expansion: "tr" matches "tr-TR" (first available variant)
59
+ *
60
+ * Returns null if no match found.
61
+ */
62
+ export declare function matchLocale(languages: string[], availableLocales: string[]): string | null;
63
+ /**
64
+ * Detect best locale from Accept-Language header (server) or navigator.languages (client).
65
+ *
66
+ * - Server (Web Request): reads `request.headers.get('accept-language')`
67
+ * - Server (Express/Hono/etc.): pass `acceptLanguage` header string directly
68
+ * - Client: reads `navigator.languages` / `navigator.language`
69
+ *
70
+ * Falls back to `defaultLocale` if no match is found.
71
+ */
72
+ export declare function detectLocale(options: {
73
+ request?: Request | null;
74
+ /** Directly pass an Accept-Language header string (e.g. from Express/Hono req.headers) */
75
+ acceptLanguage?: string | null;
76
+ availableLocales: string[];
77
+ defaultLocale: string;
78
+ }): string;
79
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAE,eAAe,EAAoB,MAAM,eAAe,CAAC;AAClE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAC7C,cAAc,EACd,eAAe,CAChB;IACC;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,QAAQ,CAAC,CAqBnB;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,eAAe,CAAC,GAAG;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GACzE,OAAO,CAAC,MAAM,EAAE,CAAC,CAenB;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,eAAe,CAAC,GAAG;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GACzE,OAAO,CAAC,cAAc,EAAE,CAAC,CAe3B;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;;WAQA;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAMrC;AAED,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAElE;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,EAAE,CAoB/E;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,MAAM,EAAE,EACnB,gBAAgB,EAAE,MAAM,EAAE,GACzB,MAAM,GAAG,IAAI,CAcf;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE;IACpC,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,MAAM,CAiBT"}
package/dist/server.js ADDED
@@ -0,0 +1,156 @@
1
+ import { createI18nCore } from "@better-i18n/core";
2
+ import { createFormatter, createTranslator } from "use-intl/core";
3
+ /**
4
+ * Fetch messages for a locale (server-side)
5
+ */
6
+ export async function getMessages(config) {
7
+ const i18n = createI18nCore({
8
+ project: config.project,
9
+ defaultLocale: config.locale,
10
+ cdnBaseUrl: config.cdnBaseUrl,
11
+ debug: config.debug,
12
+ logLevel: config.logLevel,
13
+ fetch: config.fetch,
14
+ storage: config.storage,
15
+ staticData: config.staticData,
16
+ fetchTimeout: config.fetchTimeout,
17
+ retryCount: config.retryCount,
18
+ });
19
+ const messages = (await i18n.getMessages(config.locale));
20
+ // better-i18n convention: JSON matches exact namespace structure.
21
+ // if CDN returns { "hero": { "title": "..." } }, use-intl expects exactly that
22
+ // if it's deeply nested, use-intl also handles nested objects.
23
+ return messages;
24
+ }
25
+ /**
26
+ * Fetch available locales (server-side)
27
+ */
28
+ export async function getLocales(config) {
29
+ const i18n = createI18nCore({
30
+ project: config.project,
31
+ defaultLocale: config.defaultLocale || "en",
32
+ cdnBaseUrl: config.cdnBaseUrl,
33
+ debug: config.debug,
34
+ logLevel: config.logLevel,
35
+ fetch: config.fetch,
36
+ storage: config.storage,
37
+ staticData: config.staticData,
38
+ fetchTimeout: config.fetchTimeout,
39
+ retryCount: config.retryCount,
40
+ });
41
+ return i18n.getLocales();
42
+ }
43
+ /**
44
+ * Fetch available languages with metadata (server-side)
45
+ */
46
+ export async function getLanguages(config) {
47
+ const i18n = createI18nCore({
48
+ project: config.project,
49
+ defaultLocale: config.defaultLocale || "en",
50
+ cdnBaseUrl: config.cdnBaseUrl,
51
+ debug: config.debug,
52
+ logLevel: config.logLevel,
53
+ fetch: config.fetch,
54
+ storage: config.storage,
55
+ staticData: config.staticData,
56
+ fetchTimeout: config.fetchTimeout,
57
+ retryCount: config.retryCount,
58
+ });
59
+ return i18n.getLanguages();
60
+ }
61
+ /**
62
+ * Create a translator function for use outside React (server-side)
63
+ */
64
+ export function createServerTranslator(config) {
65
+ return createTranslator({
66
+ locale: config.locale,
67
+ messages: config.messages,
68
+ namespace: config.namespace,
69
+ });
70
+ }
71
+ /**
72
+ * Create a formatter for use outside React (server-side)
73
+ */
74
+ export function createServerFormatter(config) {
75
+ return createFormatter({
76
+ locale: config.locale,
77
+ timeZone: config.timeZone,
78
+ now: config.now,
79
+ });
80
+ }
81
+ export { createTranslator, createFormatter } from "use-intl/core";
82
+ /**
83
+ * Parse an RFC 5646 Accept-Language header value into a prioritized list of language tags.
84
+ *
85
+ * @example
86
+ * parseAcceptLanguage("tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7")
87
+ * // → ["tr-TR", "tr", "en-US", "en"]
88
+ */
89
+ export function parseAcceptLanguage(header) {
90
+ if (!header)
91
+ return [];
92
+ return (header
93
+ .split(",")
94
+ .map((entry) => {
95
+ const [tag, qPart] = entry.trim().split(";");
96
+ const language = tag?.trim();
97
+ if (!language || language === "*")
98
+ return null;
99
+ const q = qPart ? parseFloat(qPart.trim().replace("q=", "")) : 1.0;
100
+ const quality = Number.isNaN(q) ? 1.0 : q;
101
+ return { language, quality };
102
+ })
103
+ .filter((item) => item !== null)
104
+ .sort((a, b) => b.quality - a.quality)
105
+ .map((item) => item.language));
106
+ }
107
+ /**
108
+ * Find the best matching locale from a parsed language list against available locales.
109
+ * Matching strategy (in order):
110
+ * 1. Exact match: "tr-TR" → "tr-TR"
111
+ * 2. Base language match: "tr-TR" → "tr"
112
+ * 3. Region expansion: "tr" matches "tr-TR" (first available variant)
113
+ *
114
+ * Returns null if no match found.
115
+ */
116
+ export function matchLocale(languages, availableLocales) {
117
+ for (const lang of languages) {
118
+ if (availableLocales.includes(lang))
119
+ return lang;
120
+ const base = lang.split("-")[0];
121
+ if (base && availableLocales.includes(base))
122
+ return base;
123
+ const expanded = availableLocales.find((l) => l === base || l.startsWith(`${base}-`));
124
+ if (expanded)
125
+ return expanded;
126
+ }
127
+ return null;
128
+ }
129
+ /**
130
+ * Detect best locale from Accept-Language header (server) or navigator.languages (client).
131
+ *
132
+ * - Server (Web Request): reads `request.headers.get('accept-language')`
133
+ * - Server (Express/Hono/etc.): pass `acceptLanguage` header string directly
134
+ * - Client: reads `navigator.languages` / `navigator.language`
135
+ *
136
+ * Falls back to `defaultLocale` if no match is found.
137
+ */
138
+ export function detectLocale(options) {
139
+ const languages = [];
140
+ if (options.request) {
141
+ languages.push(...parseAcceptLanguage(options.request.headers.get("accept-language")));
142
+ }
143
+ else if (options.acceptLanguage !== undefined) {
144
+ languages.push(...parseAcceptLanguage(options.acceptLanguage));
145
+ }
146
+ else if (typeof navigator !== "undefined") {
147
+ const navLangs = Array.isArray(navigator.languages)
148
+ ? [...navigator.languages]
149
+ : navigator.language
150
+ ? [navigator.language]
151
+ : [];
152
+ languages.push(...navLangs);
153
+ }
154
+ return matchLocale(languages, options.availableLocales) ?? options.defaultLocale;
155
+ }
156
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAalE;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAyB;IAEzB,MAAM,IAAI,GAAG,cAAc,CAAC;QAC1B,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,aAAa,EAAE,MAAM,CAAC,MAAM;QAC5B,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,YAAY,EAAE,MAAM,CAAC,YAAY;QACjC,UAAU,EAAE,MAAM,CAAC,UAAU;KAC9B,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,CAAC,MAAM,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,CAAa,CAAC;IAErE,kEAAkE;IAClE,+EAA+E;IAC/E,+DAA+D;IAE/D,OAAO,QAAoB,CAAC;AAC9B,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,MAA0E;IAE1E,MAAM,IAAI,GAAG,cAAc,CAAC;QAC1B,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,IAAI;QAC3C,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,YAAY,EAAE,MAAM,CAAC,YAAY;QACjC,UAAU,EAAE,MAAM,CAAC,UAAU;KAC9B,CAAC,CAAC;IAEH,OAAO,IAAI,CAAC,UAAU,EAAE,CAAC;AAC3B,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,MAA0E;IAE1E,MAAM,IAAI,GAAG,cAAc,CAAC;QAC1B,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,IAAI;QAC3C,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,YAAY,EAAE,MAAM,CAAC,YAAY;QACjC,UAAU,EAAE,MAAM,CAAC,UAAU;KAC9B,CAAC,CAAC;IAEH,OAAO,IAAI,CAAC,YAAY,EAAE,CAAC;AAC7B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAItC;IACC,OAAO,gBAAgB,CAAC;QACtB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,QAAQ,EAAE,MAAM,CAAC,QAED;QAChB,SAAS,EAAE,MAAM,CAAC,SAAS;KAC5B,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAIrC;IACC,OAAO,eAAe,CAAC;QACrB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,GAAG,EAAE,MAAM,CAAC,GAAG;KAChB,CAAC,CAAC;AACL,CAAC;AAED,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAElE;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAiC;IACnE,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IAEvB,OAAO,CACL,MAAM;SACH,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QACb,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC7C,MAAM,QAAQ,GAAG,GAAG,EAAE,IAAI,EAAE,CAAC;QAC7B,IAAI,CAAC,QAAQ,IAAI,QAAQ,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QAE/C,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;QACnE,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAE1C,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAC/B,CAAC,CAAC;SACD,MAAM,CAAC,CAAC,IAAI,EAAiD,EAAE,CAAC,IAAI,KAAK,IAAI,CAAC;SAC9E,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC;SACrC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAChC,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CACzB,SAAmB,EACnB,gBAA0B;IAE1B,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,IAAI,gBAAgB,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QAEjD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAChC,IAAI,IAAI,IAAI,gBAAgB,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QAEzD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,IAAI,CACpC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,CAAC,CAC9C,CAAC;QACF,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;IAChC,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,OAM5B;IACC,MAAM,SAAS,GAAa,EAAE,CAAC;IAE/B,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACpB,SAAS,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC;IACzF,CAAC;SAAM,IAAI,OAAO,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;QAChD,SAAS,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC;IACjE,CAAC;SAAM,IAAI,OAAO,SAAS,KAAK,WAAW,EAAE,CAAC;QAC5C,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,SAAS,CAAC;YACjD,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,SAAS,CAAC;YAC1B,CAAC,CAAC,SAAS,CAAC,QAAQ;gBAClB,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC;gBACtB,CAAC,CAAC,EAAE,CAAC;QACT,SAAS,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC;IAC9B,CAAC;IAED,OAAO,WAAW,CAAC,SAAS,EAAE,OAAO,CAAC,gBAAgB,CAAC,IAAI,OAAO,CAAC,aAAa,CAAC;AACnF,CAAC"}
@@ -0,0 +1,71 @@
1
+ import type { I18nCoreConfig, LanguageOption } from "@better-i18n/core";
2
+ import type { ComponentProps } from "react";
3
+ import type { IntlProvider } from "use-intl";
4
+ /**
5
+ * Messages type (compatible with use-intl)
6
+ */
7
+ export type Messages = ComponentProps<typeof IntlProvider>["messages"];
8
+ /**
9
+ * Configuration for BetterI18nProvider
10
+ */
11
+ export interface BetterI18nProviderConfig extends Omit<I18nCoreConfig, "defaultLocale"> {
12
+ /**
13
+ * Current locale
14
+ */
15
+ locale: string;
16
+ /**
17
+ * Pre-loaded messages (for SSR hydration)
18
+ */
19
+ messages?: Messages;
20
+ /**
21
+ * Timezone for date/time formatting
22
+ * @default undefined (uses browser timezone)
23
+ */
24
+ timeZone?: string;
25
+ /**
26
+ * Current date/time (useful for SSR to prevent hydration mismatches)
27
+ */
28
+ now?: Date;
29
+ /**
30
+ * Error handler for missing translations
31
+ */
32
+ onError?: ComponentProps<typeof IntlProvider>["onError"];
33
+ }
34
+ /**
35
+ * Better i18n context value
36
+ *
37
+ * Note: Locale is read-only. Use useLocaleRouter().navigate() for locale changes
38
+ * to ensure proper router integration.
39
+ */
40
+ export interface BetterI18nContextValue {
41
+ /**
42
+ * Current locale (read-only - use useLocaleRouter().navigate() to change)
43
+ */
44
+ locale: string;
45
+ /**
46
+ * Available languages with metadata from CDN manifest
47
+ */
48
+ languages: LanguageOption[];
49
+ /**
50
+ * Whether languages are still loading from CDN
51
+ */
52
+ isLoadingLanguages: boolean;
53
+ /**
54
+ * Whether messages are still loading
55
+ */
56
+ isLoadingMessages: boolean;
57
+ /**
58
+ * Project identifier
59
+ */
60
+ project: string;
61
+ }
62
+ /**
63
+ * Server-side configuration for getMessages
64
+ */
65
+ export interface GetMessagesConfig extends I18nCoreConfig {
66
+ /**
67
+ * Locale to fetch messages for
68
+ */
69
+ locale: string;
70
+ }
71
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,cAAc,CAAC,OAAO,YAAY,CAAC,CAAC,UAAU,CAAC,CAAC;AAEvE;;GAEG;AACH,MAAM,WAAW,wBACf,SAAQ,IAAI,CAAC,cAAc,EAAE,eAAe,CAAC;IAC7C;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAEpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,GAAG,CAAC,EAAE,IAAI,CAAC;IAEX;;OAEG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC,OAAO,YAAY,CAAC,CAAC,SAAS,CAAC,CAAC;CAC1D;AAED;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,SAAS,EAAE,cAAc,EAAE,CAAC;IAE5B;;OAEG;IACH,kBAAkB,EAAE,OAAO,CAAC;IAE5B;;OAEG;IACH,iBAAiB,EAAE,OAAO,CAAC;IAE3B;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAkB,SAAQ,cAAc;IACvD;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-i18n/use-intl",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "Better i18n integration for use-intl (React, TanStack Start)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -13,16 +13,28 @@
13
13
  },
14
14
  "homepage": "https://github.com/better-i18n/better-i18n/tree/main/packages/use-intl",
15
15
  "type": "module",
16
- "main": "./src/index.ts",
17
- "types": "./src/index.ts",
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
18
  "exports": {
19
- ".": "./src/index.ts",
20
- "./server": "./src/server.ts",
21
- "./middleware": "./src/middleware/index.ts",
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "bun": "./src/index.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "./server": {
25
+ "types": "./dist/server.d.ts",
26
+ "bun": "./src/server.ts",
27
+ "default": "./dist/server.js"
28
+ },
29
+ "./middleware": {
30
+ "types": "./dist/middleware/index.d.ts",
31
+ "bun": "./src/middleware/index.ts",
32
+ "default": "./dist/middleware/index.js"
33
+ },
22
34
  "./package.json": "./package.json"
23
35
  },
24
36
  "files": [
25
- "src",
37
+ "dist",
26
38
  "package.json",
27
39
  "README.md"
28
40
  ],
@@ -41,10 +53,11 @@
41
53
  "scripts": {
42
54
  "build": "tsc",
43
55
  "typecheck": "tsc --noEmit",
44
- "clean": "rm -rf dist"
56
+ "clean": "rm -rf dist",
57
+ "prepublishOnly": "bun run build"
45
58
  },
46
59
  "dependencies": {
47
- "@better-i18n/core": "0.1.8"
60
+ "@better-i18n/core": "^0.3.0"
48
61
  },
49
62
  "peerDependencies": {
50
63
  "react": ">=18.0.0",
@@ -1,76 +0,0 @@
1
- "use client";
2
-
3
- import type { ComponentProps, ReactNode } from "react";
4
- import { useLocaleRouter } from "./hooks/useLocaleRouter";
5
- import { useLanguages } from "./hooks";
6
-
7
- export interface LanguageSwitcherProps
8
- extends Omit<ComponentProps<"select">, "value" | "onChange" | "children"> {
9
- /**
10
- * Render function for custom option display
11
- */
12
- renderOption?: (language: {
13
- code: string;
14
- name?: string;
15
- nativeName?: string;
16
- flagUrl?: string | null;
17
- isDefault?: boolean;
18
- }) => ReactNode;
19
-
20
- /**
21
- * Label for loading state
22
- */
23
- loadingLabel?: string;
24
- }
25
-
26
- /**
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.
31
- *
32
- * @example
33
- * ```tsx
34
- * // Basic usage - just works!
35
- * <LanguageSwitcher />
36
- *
37
- * // With custom styling
38
- * <LanguageSwitcher className="my-select" />
39
- *
40
- * // Custom option rendering
41
- * <LanguageSwitcher
42
- * renderOption={(lang) => (
43
- * <>
44
- * {lang.flagUrl && <img src={lang.flagUrl} alt="" />}
45
- * {lang.nativeName}
46
- * </>
47
- * )}
48
- * />
49
- * ```
50
- */
51
- export function LanguageSwitcher({
52
- renderOption,
53
- loadingLabel = "Loading...",
54
- ...props
55
- }: LanguageSwitcherProps) {
56
- const { locale, navigate, isReady } = useLocaleRouter();
57
- const { languages } = useLanguages();
58
-
59
- if (!isReady) {
60
- return (
61
- <select disabled {...props}>
62
- <option>{loadingLabel}</option>
63
- </select>
64
- );
65
- }
66
-
67
- return (
68
- <select value={locale} onChange={(e) => navigate(e.target.value)} {...props}>
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
- }