@better-i18n/next 0.4.0 → 0.5.1

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/next",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Better-i18n Next.js integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -25,8 +25,8 @@
25
25
  "default": "./src/server.ts"
26
26
  },
27
27
  "./client": {
28
- "types": "./src/client.ts",
29
- "default": "./src/client.ts"
28
+ "types": "./src/client.tsx",
29
+ "default": "./src/client.tsx"
30
30
  },
31
31
  "./middleware": {
32
32
  "types": "./src/middleware.ts",
@@ -57,7 +57,7 @@
57
57
  "typecheck": "tsc --noEmit"
58
58
  },
59
59
  "dependencies": {
60
- "@better-i18n/core": "0.1.6"
60
+ "@better-i18n/core": "0.1.7"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "next": ">=15.0.0",
package/src/client.tsx ADDED
@@ -0,0 +1,324 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useState,
10
+ type ReactNode,
11
+ } from "react";
12
+ import { useRouter } from "next/navigation";
13
+ import { NextIntlClientProvider } from "next-intl";
14
+ import { createI18nCore, parseProject } from "@better-i18n/core";
15
+ import type { LanguageOption, Messages } from "@better-i18n/core";
16
+
17
+ import { normalizeConfig } from "./config";
18
+ import type { I18nConfig } from "./types";
19
+
20
+ // ─── BetterI18nProvider ──────────────────────────────────────────────
21
+
22
+ interface BetterI18nContextValue {
23
+ setLocale: (locale: string) => void;
24
+ }
25
+
26
+ const BetterI18nContext = createContext<BetterI18nContextValue | null>(null);
27
+
28
+ export interface BetterI18nProviderProps {
29
+ /** Initial locale from server (getLocale()) */
30
+ locale: string;
31
+ /** Initial messages from server (getMessages()) */
32
+ messages: Messages;
33
+ /** i18n config — only project and defaultLocale are required */
34
+ config: I18nConfig;
35
+ children: ReactNode;
36
+ }
37
+
38
+ /**
39
+ * Provider that wraps `NextIntlClientProvider` and enables instant locale
40
+ * switching without a server round-trip.
41
+ *
42
+ * When `useSetLocale()` is called inside this provider, it:
43
+ * 1. Sets a cookie (for server-side persistence on next navigation)
44
+ * 2. Fetches new messages from CDN on the client
45
+ * 3. Re-renders the tree with new locale + messages instantly
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * // app/layout.tsx
50
+ * import { BetterI18nProvider } from '@better-i18n/next/client'
51
+ *
52
+ * export default async function RootLayout({ children }) {
53
+ * const locale = await getLocale()
54
+ * const messages = await getMessages()
55
+ *
56
+ * return (
57
+ * <BetterI18nProvider
58
+ * locale={locale}
59
+ * messages={messages}
60
+ * config={{ project: 'acme/dashboard', defaultLocale: 'en' }}
61
+ * >
62
+ * {children}
63
+ * </BetterI18nProvider>
64
+ * )
65
+ * }
66
+ * ```
67
+ */
68
+ export function BetterI18nProvider({
69
+ locale: initialLocale,
70
+ messages: initialMessages,
71
+ config,
72
+ children,
73
+ }: BetterI18nProviderProps) {
74
+ const normalized = useMemo(() => normalizeConfig(config), [
75
+ config.project,
76
+ config.defaultLocale,
77
+ config.cookieName,
78
+ config.cdnBaseUrl,
79
+ ]);
80
+
81
+ const [locale, setLocaleState] = useState(initialLocale);
82
+ const [messages, setMessages] = useState<Messages>(initialMessages);
83
+
84
+ // Sync with server-provided values when they change (e.g. navigation to a new page)
85
+ useEffect(() => {
86
+ setLocaleState(initialLocale);
87
+ setMessages(initialMessages);
88
+ }, [initialLocale, initialMessages]);
89
+
90
+ const setLocale = useCallback(
91
+ async (newLocale: string) => {
92
+ if (newLocale === locale) return;
93
+
94
+ // 1. Set cookie for server-side persistence
95
+ document.cookie = `${normalized.cookieName}=${newLocale}; path=/; max-age=31536000; samesite=lax`;
96
+
97
+ // 2. Fetch new messages from CDN
98
+ const { workspaceId, projectSlug } = parseProject(normalized.project);
99
+ const cdnBase = normalized.cdnBaseUrl || "https://cdn.better-i18n.com";
100
+ const url = `${cdnBase}/${workspaceId}/${projectSlug}/${newLocale}/translations.json`;
101
+
102
+ try {
103
+ const res = await fetch(url);
104
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
105
+ const newMessages: Messages = await res.json();
106
+
107
+ // 3. Instant client-side update — no server round-trip
108
+ setLocaleState(newLocale);
109
+ setMessages(newMessages);
110
+ } catch (err) {
111
+ console.error("[better-i18n] Client-side locale switch failed, falling back to refresh:", err);
112
+ // Fallback: let the server handle it
113
+ window.location.reload();
114
+ }
115
+ },
116
+ [locale, normalized]
117
+ );
118
+
119
+ return (
120
+ <BetterI18nContext.Provider value={{ setLocale }}>
121
+ <NextIntlClientProvider locale={locale} messages={messages}>
122
+ {children}
123
+ </NextIntlClientProvider>
124
+ </BetterI18nContext.Provider>
125
+ );
126
+ }
127
+
128
+ // ─── useManifestLanguages ────────────────────────────────────────────
129
+
130
+ export type UseManifestLanguagesResult = {
131
+ languages: LanguageOption[];
132
+ isLoading: boolean;
133
+ error: Error | null;
134
+ };
135
+
136
+ type ClientCacheEntry = {
137
+ data?: LanguageOption[];
138
+ error?: Error;
139
+ promise?: Promise<LanguageOption[]>;
140
+ };
141
+
142
+ // Client-side request deduplication cache
143
+ const clientCache = new Map<string, ClientCacheEntry>();
144
+
145
+ const getCacheKey = (project: string, cdnBaseUrl?: string) =>
146
+ `${cdnBaseUrl || "https://cdn.better-i18n.com"}|${project}`;
147
+
148
+ /**
149
+ * React hook to fetch manifest languages on the client
150
+ *
151
+ * Uses `createI18nCore` from `@better-i18n/core` internally with
152
+ * request deduplication to prevent duplicate fetches.
153
+ *
154
+ * @example
155
+ * ```tsx
156
+ * const { languages, isLoading, error } = useManifestLanguages({
157
+ * project: 'acme/dashboard',
158
+ * defaultLocale: 'en',
159
+ * })
160
+ *
161
+ * if (isLoading) return <Spinner />
162
+ * if (error) return <Error message={error.message} />
163
+ *
164
+ * return (
165
+ * <select>
166
+ * {languages.map(lang => (
167
+ * <option key={lang.code} value={lang.code}>
168
+ * {lang.nativeName || lang.name || lang.code}
169
+ * </option>
170
+ * ))}
171
+ * </select>
172
+ * )
173
+ * ```
174
+ */
175
+ export const useManifestLanguages = (config: I18nConfig): UseManifestLanguagesResult => {
176
+ const normalized = useMemo(
177
+ () => normalizeConfig(config),
178
+ [
179
+ config.project,
180
+ config.defaultLocale,
181
+ config.cdnBaseUrl,
182
+ config.debug,
183
+ config.logLevel,
184
+ ],
185
+ );
186
+
187
+ const i18nCore = useMemo(
188
+ () =>
189
+ createI18nCore({
190
+ project: normalized.project,
191
+ defaultLocale: normalized.defaultLocale,
192
+ cdnBaseUrl: normalized.cdnBaseUrl,
193
+ debug: normalized.debug,
194
+ logLevel: normalized.logLevel,
195
+ }),
196
+ [normalized],
197
+ );
198
+
199
+ const cacheKey = getCacheKey(normalized.project, normalized.cdnBaseUrl);
200
+ const cached = clientCache.get(cacheKey);
201
+
202
+ const [languages, setLanguages] = useState<LanguageOption[]>(cached?.data ?? []);
203
+ const [isLoading, setIsLoading] = useState(!cached?.data);
204
+ const [error, setError] = useState<Error | null>(cached?.error ?? null);
205
+
206
+ useEffect(() => {
207
+ let isMounted = true;
208
+
209
+ const run = async () => {
210
+ const entry = clientCache.get(cacheKey) ?? {};
211
+
212
+ // Return cached data if available
213
+ if (entry.data) {
214
+ if (isMounted) {
215
+ setLanguages(entry.data);
216
+ setIsLoading(false);
217
+ }
218
+ return;
219
+ }
220
+
221
+ // Deduplicate in-flight requests
222
+ if (!entry.promise) {
223
+ entry.promise = i18nCore
224
+ .getLanguages()
225
+ .then((langs) => {
226
+ clientCache.set(cacheKey, { data: langs });
227
+ return langs;
228
+ })
229
+ .catch((err) => {
230
+ const error = err instanceof Error ? err : new Error(String(err));
231
+ clientCache.set(cacheKey, { error });
232
+ throw error;
233
+ });
234
+ clientCache.set(cacheKey, entry);
235
+ }
236
+
237
+ try {
238
+ const langs = await entry.promise;
239
+ if (isMounted) {
240
+ setLanguages(langs);
241
+ setError(null);
242
+ }
243
+ } catch (err) {
244
+ if (isMounted) {
245
+ setError(err instanceof Error ? err : new Error(String(err)));
246
+ }
247
+ } finally {
248
+ if (isMounted) {
249
+ setIsLoading(false);
250
+ }
251
+ }
252
+ };
253
+
254
+ run();
255
+
256
+ return () => {
257
+ isMounted = false;
258
+ };
259
+ }, [cacheKey, i18nCore]);
260
+
261
+ return { languages, isLoading, error };
262
+ };
263
+
264
+ // ─── useSetLocale ────────────────────────────────────────────────────
265
+
266
+ /**
267
+ * React hook that returns a function to switch the active locale.
268
+ *
269
+ * - **With `BetterI18nProvider`**: Fetches new messages client-side from CDN
270
+ * and updates the UI instantly — no page refresh needed.
271
+ * - **Without provider (standalone)**: Pass a config object. Sets a cookie and
272
+ * calls `router.refresh()` for a soft server re-render.
273
+ *
274
+ * @example With provider (recommended — instant switching)
275
+ * ```tsx
276
+ * // Wrap your layout with BetterI18nProvider, then:
277
+ * const setLocale = useSetLocale()
278
+ * <button onClick={() => setLocale('tr')}>Türkçe</button>
279
+ * ```
280
+ *
281
+ * @example Standalone (soft refresh)
282
+ * ```tsx
283
+ * const setLocale = useSetLocale({ project: 'acme/dashboard', defaultLocale: 'en' })
284
+ * <button onClick={() => setLocale('tr')}>Türkçe</button>
285
+ * ```
286
+ */
287
+ export function useSetLocale(config?: I18nConfig): (locale: string) => void {
288
+ const ctx = useContext(BetterI18nContext);
289
+
290
+ // If BetterI18nProvider is in the tree, use instant client-side switching
291
+ if (ctx) {
292
+ return ctx.setLocale;
293
+ }
294
+
295
+ // Standalone fallback: cookie + router.refresh()
296
+ if (!config) {
297
+ throw new Error(
298
+ "[better-i18n] useSetLocale() requires either a <BetterI18nProvider> ancestor " +
299
+ "or a config argument. Use useSetLocale({ project, defaultLocale }) for standalone mode."
300
+ );
301
+ }
302
+
303
+ // eslint-disable-next-line react-hooks/rules-of-hooks
304
+ return useStandaloneSetLocale(config);
305
+ }
306
+
307
+ /** Internal standalone hook — cookie + router.refresh() */
308
+ function useStandaloneSetLocale(config: I18nConfig) {
309
+ const normalized = useMemo(() => normalizeConfig(config), [
310
+ config.project,
311
+ config.defaultLocale,
312
+ config.cookieName,
313
+ ]);
314
+
315
+ const router = useRouter();
316
+
317
+ return useCallback(
318
+ (locale: string) => {
319
+ document.cookie = `${normalized.cookieName}=${locale}; path=/; max-age=31536000; samesite=lax`;
320
+ router.refresh();
321
+ },
322
+ [normalized.cookieName, router]
323
+ );
324
+ }
package/src/config.ts CHANGED
@@ -27,6 +27,7 @@ export const normalizeConfig = (config: I18nConfig): NormalizedConfig => {
27
27
  return {
28
28
  ...coreConfig,
29
29
  localePrefix: config.localePrefix ?? "as-needed",
30
+ cookieName: config.cookieName ?? "locale",
30
31
  manifestRevalidateSeconds: config.manifestRevalidateSeconds ?? 3600,
31
32
  messagesRevalidateSeconds: config.messagesRevalidateSeconds ?? 30,
32
33
  };
package/src/index.ts CHANGED
@@ -81,6 +81,7 @@ export const createI18n = (config: I18nConfig) => {
81
81
  detection: {
82
82
  cookie: true,
83
83
  browserLanguage: true,
84
+ cookieName: normalized.cookieName,
84
85
  },
85
86
  },
86
87
  callback
@@ -96,8 +97,9 @@ export type { MiddlewareContext, MiddlewareCallback } from "./middleware";
96
97
  // Core instance factory
97
98
  export { createNextI18nCore } from "./server";
98
99
 
99
- // Client hook
100
- export { useManifestLanguages } from "./client";
100
+ // Client hooks & provider
101
+ export { BetterI18nProvider, useManifestLanguages, useSetLocale } from "./client";
102
+ export type { BetterI18nProviderProps } from "./client";
101
103
 
102
104
  // Re-export types
103
105
  export type {
package/src/middleware.ts CHANGED
@@ -107,19 +107,12 @@ export function createBetterI18nMiddleware(
107
107
  // 1. Fetch available locales from CDN
108
108
  const availableLocales = await i18nCore.getLocales();
109
109
 
110
- // 2. Create/reuse next-intl middleware (only recreate if locales changed)
111
- const localesKey = availableLocales.join(",");
112
- if (!cachedMiddleware || cachedLocalesKey !== localesKey) {
113
- cachedMiddleware = createMiddleware({
114
- locales: availableLocales,
115
- defaultLocale,
116
- localePrefix,
117
- });
118
- cachedLocalesKey = localesKey;
119
- }
120
-
121
- // 3. Our custom locale detection (for cookie logic)
122
- const pathLocale = request.nextUrl.pathname.split("/")[1];
110
+ // 2. Our custom locale detection
111
+ // When localePrefix is "never", path has no locale segment — skip it
112
+ const pathLocale =
113
+ localePrefix === "never"
114
+ ? null
115
+ : request.nextUrl.pathname.split("/")[1];
123
116
  const cookieLocale = cookie ? request.cookies.get(cookieName)?.value : null;
124
117
  const headerLocale = browserLanguage
125
118
  ? request.headers.get("accept-language")?.split(",")[0]?.split("-")[0]
@@ -134,18 +127,37 @@ export function createBetterI18nMiddleware(
134
127
  availableLocales,
135
128
  });
136
129
 
137
- // 4. Call next-intl middleware (sets x-middleware-request-x-next-intl-locale header)
138
- const response = cachedMiddleware(request);
139
-
140
- // 5. Get detected locale from next-intl header (more accurate than our detection)
141
- const detectedLocale =
142
- response.headers.get("x-middleware-request-x-next-intl-locale") ||
143
- result.locale;
130
+ const detectedLocale = result.locale;
131
+
132
+ let response: NextResponse;
133
+
134
+ if (localePrefix === "never") {
135
+ // 3a. "never" mode: bypass next-intl middleware entirely.
136
+ // next-intl's createMiddleware tries URL rewriting which breaks cookie-only locale.
137
+ // We create a plain response and set the header that next-intl's getRequestConfig reads.
138
+ response = NextResponse.next();
139
+ response.headers.set(
140
+ "x-middleware-request-x-next-intl-locale",
141
+ detectedLocale
142
+ );
143
+ } else {
144
+ // 3b. URL-based modes: delegate to next-intl middleware as before
145
+ const localesKey = availableLocales.join(",");
146
+ if (!cachedMiddleware || cachedLocalesKey !== localesKey) {
147
+ cachedMiddleware = createMiddleware({
148
+ locales: availableLocales,
149
+ defaultLocale,
150
+ localePrefix,
151
+ });
152
+ cachedLocalesKey = localesKey;
153
+ }
154
+ response = cachedMiddleware(request);
155
+ }
144
156
 
145
- // 6. Add x-locale header for backwards compatibility
157
+ // 4. Add x-locale header for backwards compatibility
146
158
  response.headers.set("x-locale", detectedLocale);
147
159
 
148
- // 7. Set our custom cookie if needed
160
+ // 5. Set our custom cookie if needed
149
161
  if (cookie && result.shouldSetCookie) {
150
162
  response.cookies.set(cookieName, detectedLocale, {
151
163
  path: "/",
@@ -154,7 +166,7 @@ export function createBetterI18nMiddleware(
154
166
  });
155
167
  }
156
168
 
157
- // 8. If callback provided, execute it (Clerk-style)
169
+ // 6. If callback provided, execute it (Clerk-style)
158
170
  if (callback) {
159
171
  const callbackResult = await callback(request, {
160
172
  locale: detectedLocale,
@@ -167,7 +179,7 @@ export function createBetterI18nMiddleware(
167
179
  }
168
180
  }
169
181
 
170
- // 9. Return the i18n response (with all headers preserved)
182
+ // 7. Return the response
171
183
  return response;
172
184
  };
173
185
  }
package/src/server.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { getRequestConfig } from "next-intl/server";
2
+ import { cookies } from "next/headers";
2
3
  import { createI18nCore } from "@better-i18n/core";
3
4
  import type { I18nCore, Messages } from "@better-i18n/core";
4
5
 
@@ -98,8 +99,25 @@ export const createNextIntlRequestConfig = (config: I18nConfig) =>
98
99
  const i18n = createNextI18nCore(config);
99
100
  const normalized = normalizeConfig(config);
100
101
  const locales = await i18n.getLocales();
102
+
103
+ // 1. Middleware header (set by next-intl or our middleware)
101
104
  let locale = await requestLocale;
102
105
 
106
+ // 2. Cookie fallback — critical for localePrefix: "never" where
107
+ // requestLocale may be undefined if middleware header isn't forwarded
108
+ if (!locale) {
109
+ try {
110
+ const cookieStore = await cookies();
111
+ const cookieLocale = cookieStore.get(normalized.cookieName)?.value;
112
+ if (cookieLocale && locales.includes(cookieLocale)) {
113
+ locale = cookieLocale;
114
+ }
115
+ } catch {
116
+ // cookies() throws in non-request contexts (e.g. build time)
117
+ }
118
+ }
119
+
120
+ // 3. Final fallback to defaultLocale
103
121
  if (!locale || !locales.includes(locale)) {
104
122
  locale = normalized.defaultLocale;
105
123
  }
package/src/types.ts CHANGED
@@ -27,6 +27,12 @@ export interface I18nConfig extends I18nCoreConfig {
27
27
  */
28
28
  localePrefix?: LocalePrefix;
29
29
 
30
+ /**
31
+ * Cookie name used for locale persistence when localePrefix is "never"
32
+ * @default "locale"
33
+ */
34
+ cookieName?: string;
35
+
30
36
  /**
31
37
  * Next.js ISR revalidation time for manifest (seconds)
32
38
  * @default 3600
@@ -45,6 +51,7 @@ export interface I18nConfig extends I18nCoreConfig {
45
51
  */
46
52
  export interface NormalizedConfig extends CoreNormalizedConfig, Omit<I18nConfig, keyof I18nCoreConfig> {
47
53
  localePrefix: LocalePrefix;
54
+ cookieName: string;
48
55
  manifestRevalidateSeconds: number;
49
56
  messagesRevalidateSeconds: number;
50
57
  }
package/src/client.ts DELETED
@@ -1,142 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect, useMemo, useState } from "react";
4
- import { createI18nCore } from "@better-i18n/core";
5
- import type { LanguageOption } from "@better-i18n/core";
6
-
7
- import { normalizeConfig } from "./config";
8
- import type { I18nConfig } from "./types";
9
-
10
- export type UseManifestLanguagesResult = {
11
- languages: LanguageOption[];
12
- isLoading: boolean;
13
- error: Error | null;
14
- };
15
-
16
- type ClientCacheEntry = {
17
- data?: LanguageOption[];
18
- error?: Error;
19
- promise?: Promise<LanguageOption[]>;
20
- };
21
-
22
- // Client-side request deduplication cache
23
- const clientCache = new Map<string, ClientCacheEntry>();
24
-
25
- const getCacheKey = (project: string, cdnBaseUrl?: string) =>
26
- `${cdnBaseUrl || "https://cdn.better-i18n.com"}|${project}`;
27
-
28
- /**
29
- * React hook to fetch manifest languages on the client
30
- *
31
- * Uses `createI18nCore` from `@better-i18n/core` internally with
32
- * request deduplication to prevent duplicate fetches.
33
- *
34
- * @example
35
- * ```tsx
36
- * const { languages, isLoading, error } = useManifestLanguages({
37
- * project: 'acme/dashboard',
38
- * defaultLocale: 'en',
39
- * })
40
- *
41
- * if (isLoading) return <Spinner />
42
- * if (error) return <Error message={error.message} />
43
- *
44
- * return (
45
- * <select>
46
- * {languages.map(lang => (
47
- * <option key={lang.code} value={lang.code}>
48
- * {lang.nativeName || lang.name || lang.code}
49
- * </option>
50
- * ))}
51
- * </select>
52
- * )
53
- * ```
54
- */
55
- export const useManifestLanguages = (config: I18nConfig): UseManifestLanguagesResult => {
56
- const normalized = useMemo(
57
- () => normalizeConfig(config),
58
- [
59
- config.project,
60
- config.defaultLocale,
61
- config.cdnBaseUrl,
62
- config.debug,
63
- config.logLevel,
64
- ],
65
- );
66
-
67
- const i18nCore = useMemo(
68
- () =>
69
- createI18nCore({
70
- project: normalized.project,
71
- defaultLocale: normalized.defaultLocale,
72
- cdnBaseUrl: normalized.cdnBaseUrl,
73
- debug: normalized.debug,
74
- logLevel: normalized.logLevel,
75
- }),
76
- [normalized],
77
- );
78
-
79
- const cacheKey = getCacheKey(normalized.project, normalized.cdnBaseUrl);
80
- const cached = clientCache.get(cacheKey);
81
-
82
- const [languages, setLanguages] = useState<LanguageOption[]>(cached?.data ?? []);
83
- const [isLoading, setIsLoading] = useState(!cached?.data);
84
- const [error, setError] = useState<Error | null>(cached?.error ?? null);
85
-
86
- useEffect(() => {
87
- let isMounted = true;
88
-
89
- const run = async () => {
90
- const entry = clientCache.get(cacheKey) ?? {};
91
-
92
- // Return cached data if available
93
- if (entry.data) {
94
- if (isMounted) {
95
- setLanguages(entry.data);
96
- setIsLoading(false);
97
- }
98
- return;
99
- }
100
-
101
- // Deduplicate in-flight requests
102
- if (!entry.promise) {
103
- entry.promise = i18nCore
104
- .getLanguages()
105
- .then((langs) => {
106
- clientCache.set(cacheKey, { data: langs });
107
- return langs;
108
- })
109
- .catch((err) => {
110
- const error = err instanceof Error ? err : new Error(String(err));
111
- clientCache.set(cacheKey, { error });
112
- throw error;
113
- });
114
- clientCache.set(cacheKey, entry);
115
- }
116
-
117
- try {
118
- const langs = await entry.promise;
119
- if (isMounted) {
120
- setLanguages(langs);
121
- setError(null);
122
- }
123
- } catch (err) {
124
- if (isMounted) {
125
- setError(err instanceof Error ? err : new Error(String(err)));
126
- }
127
- } finally {
128
- if (isMounted) {
129
- setIsLoading(false);
130
- }
131
- }
132
- };
133
-
134
- run();
135
-
136
- return () => {
137
- isMounted = false;
138
- };
139
- }, [cacheKey, i18nCore]);
140
-
141
- return { languages, isLoading, error };
142
- };