i18n-keyless-react 1.18.0 → 2.1.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.
@@ -0,0 +1,48 @@
1
+ import React from "react";
2
+ import { type Lang, type Translations } from "i18n-keyless-core";
3
+ export interface I18nKeylessContextValue {
4
+ /**
5
+ * The language this subtree renders in (typically derived from the URL / Accept-Language).
6
+ */
7
+ lang: Lang;
8
+ /**
9
+ * The translations map for `lang`, typically produced by `getServerTranslations(lang)`.
10
+ */
11
+ translations: Translations;
12
+ }
13
+ /**
14
+ * Returns the nearest `I18nKeylessProvider` value, or `null` when none is present.
15
+ * When `null`, `<I18nKeylessText>` falls back to the global zustand store (SPA mode).
16
+ */
17
+ export declare function useI18nKeylessContext(): I18nKeylessContextValue | null;
18
+ export interface I18nKeylessProviderProps {
19
+ /**
20
+ * The language to render in for this subtree (typically from the URL: `/{lang}/...`
21
+ * or `?lang={lang}`, or from `Accept-Language`).
22
+ */
23
+ lang: Lang;
24
+ /**
25
+ * The translations map for `lang`. On the server, produce it with
26
+ * `getServerTranslations(lang)`; serialize it into the HTML and pass the same map
27
+ * here on the client so the first client render matches the server output.
28
+ */
29
+ translations: Translations;
30
+ children: React.ReactNode;
31
+ }
32
+ /**
33
+ * Per-request language provider for SSR.
34
+ *
35
+ * When present, `<I18nKeylessText>` ("`<T>`") reads `lang` and `translations` from
36
+ * this context instead of the module-scope store. This is what lets a single server
37
+ * render produce HTML in a chosen non-primary language without leaking language state
38
+ * across concurrent requests (the store is a process-wide singleton; the context is
39
+ * per-render).
40
+ *
41
+ * On the client it additionally seeds the global store once on mount, so store-based
42
+ * consumers (e.g. `useCurrentLanguage`) stay consistent and there is no flash after
43
+ * hydration.
44
+ *
45
+ * In provider mode the language is controlled by the `lang` prop (drive it from the
46
+ * URL). `setCurrentLanguage` is for non-provider SPA mode. See docs/SSR.md.
47
+ */
48
+ export declare const I18nKeylessProvider: React.FC<I18nKeylessProviderProps>;
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect } from "react";
3
+ import { useI18nKeyless } from "./store.js";
4
+ const I18nKeylessContext = createContext(null);
5
+ /**
6
+ * Returns the nearest `I18nKeylessProvider` value, or `null` when none is present.
7
+ * When `null`, `<I18nKeylessText>` falls back to the global zustand store (SPA mode).
8
+ */
9
+ export function useI18nKeylessContext() {
10
+ return useContext(I18nKeylessContext);
11
+ }
12
+ /**
13
+ * Per-request language provider for SSR.
14
+ *
15
+ * When present, `<I18nKeylessText>` ("`<T>`") reads `lang` and `translations` from
16
+ * this context instead of the module-scope store. This is what lets a single server
17
+ * render produce HTML in a chosen non-primary language without leaking language state
18
+ * across concurrent requests (the store is a process-wide singleton; the context is
19
+ * per-render).
20
+ *
21
+ * On the client it additionally seeds the global store once on mount, so store-based
22
+ * consumers (e.g. `useCurrentLanguage`) stay consistent and there is no flash after
23
+ * hydration.
24
+ *
25
+ * In provider mode the language is controlled by the `lang` prop (drive it from the
26
+ * URL). `setCurrentLanguage` is for non-provider SPA mode. See docs/SSR.md.
27
+ */
28
+ export const I18nKeylessProvider = ({ lang, translations, children }) => {
29
+ // Client-only (effects don't run during SSR): seed the global store so reads after
30
+ // hydration match the server-rendered, context-driven output.
31
+ useEffect(() => {
32
+ useI18nKeyless.setState((state) => ({
33
+ currentLanguage: lang,
34
+ translations: { ...state.translations, ...translations },
35
+ }));
36
+ }, [lang, translations]);
37
+ return _jsx(I18nKeylessContext.Provider, { value: { lang, translations }, children: children });
38
+ };
@@ -1,6 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React, { useEffect, useMemo } from "react";
3
- import { useI18nKeyless, getTranslation } from "./store";
3
+ import { useI18nKeyless, getTranslation } from "./store.js";
4
+ import { useI18nKeylessContext } from "./I18nKeylessProvider.js";
5
+ import { getRequestScope } from "./request-scope.js";
4
6
  const warnAboutWhitespace = (text) => {
5
7
  if (process.env.NODE_ENV === "development" && text !== text.trim()) {
6
8
  console.warn(`I18nKeylessText received text with leading/trailing whitespace: "${text}". ` +
@@ -8,9 +10,16 @@ const warnAboutWhitespace = (text) => {
8
10
  }
9
11
  };
10
12
  export const I18nKeylessText = ({ children, replace, context, debug = false, forceTemporary }) => {
11
- const translations = useI18nKeyless((store) => store.translations);
12
- const currentLanguage = useI18nKeyless((store) => store.currentLanguage);
13
+ const storeTranslations = useI18nKeyless((store) => store.translations);
14
+ const storeCurrentLanguage = useI18nKeyless((store) => store.currentLanguage);
13
15
  const config = useI18nKeyless((store) => store.config);
16
+ // In SSR mode, language and translations come from the per-request scope so concurrent
17
+ // requests don't share state: the React-context provider first, then the
18
+ // AsyncLocalStorage request scope (set by runWithI18nKeyless). Otherwise (SPA mode)
19
+ // both are absent and we use the global store. See docs/SSR.md.
20
+ const requestScope = useI18nKeylessContext() ?? getRequestScope();
21
+ const translations = requestScope?.translations ?? storeTranslations;
22
+ const currentLanguage = requestScope?.lang ?? storeCurrentLanguage;
14
23
  // Trim the source text immediately
15
24
  const rawText = Array.isArray(children) ? children.join("") : String(children ?? "");
16
25
  const sourceText = rawText.trim();
package/dist/index.d.ts CHANGED
@@ -1,6 +1,12 @@
1
- export { I18nKeylessText, I18nKeylessText as T } from "./I18nKeylessText";
2
- export { init, setCurrentLanguage, useCurrentLanguage, getTranslation, getSupportedLanguages, useI18nKeyless } from "./store";
3
- export { clearI18nKeylessStorage, validateLanguage } from "./utils";
4
- export type { I18nKeylessTextProps } from "./I18nKeylessText";
5
- export { type I18nConfig, type TranslationStoreState, type TranslationOptions, type TranslationStore } from "./types";
1
+ export { I18nKeylessText, I18nKeylessText as T } from "./I18nKeylessText.tsx";
2
+ export { init, setCurrentLanguage, useCurrentLanguage, getTranslation, getSupportedLanguages, useI18nKeyless } from "./store.ts";
3
+ export { clearI18nKeylessStorage, validateLanguage, createMemoryStorage } from "./utils.ts";
4
+ export { I18nKeylessProvider, useI18nKeylessContext } from "./I18nKeylessProvider.tsx";
5
+ export type { I18nKeylessProviderProps } from "./I18nKeylessProvider.tsx";
6
+ export { getServerTranslations, clearServerTranslationsCache } from "./server.ts";
7
+ export { runWithI18nKeyless, getRequestScope } from "./request-scope.ts";
8
+ export type { I18nRequestScope } from "./request-scope.ts";
9
+ export { clearI18nKeylessStorageAndStore } from "./store.ts";
10
+ export type { I18nKeylessTextProps } from "./I18nKeylessText.tsx";
11
+ export { type I18nConfig, type TranslationStoreState, type TranslationOptions, type TranslationStore } from "./types.ts";
6
12
  export { AVAILABLE_LANGS, type Lang, type PrimaryLang, type Translations, type I18nKeylessRequestBody, type I18nKeylessResponse, getAllTranslationsFromLanguage, queue } from "i18n-keyless-core";
package/dist/index.js CHANGED
@@ -1,4 +1,8 @@
1
- export { I18nKeylessText, I18nKeylessText as T } from "./I18nKeylessText";
2
- export { init, setCurrentLanguage, useCurrentLanguage, getTranslation, getSupportedLanguages, useI18nKeyless } from "./store";
3
- export { clearI18nKeylessStorage, validateLanguage } from "./utils";
1
+ export { I18nKeylessText, I18nKeylessText as T } from "./I18nKeylessText.js";
2
+ export { init, setCurrentLanguage, useCurrentLanguage, getTranslation, getSupportedLanguages, useI18nKeyless } from "./store.js";
3
+ export { clearI18nKeylessStorage, validateLanguage, createMemoryStorage } from "./utils.js";
4
+ export { I18nKeylessProvider, useI18nKeylessContext } from "./I18nKeylessProvider.js";
5
+ export { getServerTranslations, clearServerTranslationsCache } from "./server.js";
6
+ export { runWithI18nKeyless, getRequestScope } from "./request-scope.js";
7
+ export { clearI18nKeylessStorageAndStore } from "./store.js";
4
8
  export { AVAILABLE_LANGS, getAllTranslationsFromLanguage, queue } from "i18n-keyless-core";
@@ -0,0 +1,36 @@
1
+ import type { Lang, Translations } from "i18n-keyless-core";
2
+ /**
3
+ * Per-request translation scope for SSR.
4
+ *
5
+ * Set by `runWithI18nKeyless` and read by both `getTranslation(...)` and
6
+ * `<I18nKeylessText>` so a single server render resolves in `lang` using `translations`,
7
+ * without touching the process-global store and without leaking across concurrent
8
+ * requests. See docs/SSR.md.
9
+ */
10
+ export interface I18nRequestScope {
11
+ lang: Lang;
12
+ translations: Translations;
13
+ }
14
+ /**
15
+ * Runs `fn` with a per-request translation scope active. Every `getTranslation(...)`
16
+ * call and every `<I18nKeylessText>` rendered within `fn` — synchronously OR across
17
+ * `await`s/streaming — resolves in `scope.lang` using `scope.translations`, with no
18
+ * change to the global store and full isolation between concurrent requests.
19
+ *
20
+ * Wrap your server render in it and await the result:
21
+ *
22
+ * const html = await runWithI18nKeyless(
23
+ * { lang, translations },
24
+ * () => renderToString(<App />)
25
+ * );
26
+ *
27
+ * Server-only. In the browser (or where AsyncLocalStorage is unavailable) it simply
28
+ * calls `fn` with no scoping. See docs/SSR.md.
29
+ */
30
+ export declare function runWithI18nKeyless<R>(scope: I18nRequestScope, fn: () => R): Promise<R>;
31
+ /**
32
+ * Returns the active per-request scope, or `undefined` when none is set (browser, or
33
+ * outside `runWithI18nKeyless`). Read internally by `getTranslation` and
34
+ * `<I18nKeylessText>`; exported for advanced/server use.
35
+ */
36
+ export declare function getRequestScope(): I18nRequestScope | undefined;
@@ -0,0 +1,63 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ // A single AsyncLocalStorage instance, created lazily on the server only. Each
10
+ // `run()` creates an isolated store, so the instance is correctly a module singleton.
11
+ let als;
12
+ let alsInit;
13
+ async function ensureALS() {
14
+ // Browser: no request scoping (single user → the store is correct). Also avoids
15
+ // pulling a Node builtin into client bundles.
16
+ if (als || typeof window !== "undefined") {
17
+ return;
18
+ }
19
+ if (!alsInit) {
20
+ alsInit = (async () => {
21
+ try {
22
+ // Variable specifier + ignore hints keep bundlers from trying to resolve a Node
23
+ // builtin into the browser graph. tsc keeps these comments (removeComments=false).
24
+ const specifier = "node:async_hooks";
25
+ const mod = (await import(__rewriteRelativeImportExtension(/* @vite-ignore */ /* webpackIgnore: true */ specifier)));
26
+ als = new mod.AsyncLocalStorage();
27
+ }
28
+ catch {
29
+ // AsyncLocalStorage unavailable (e.g. some edge runtimes). Scoping degrades to a
30
+ // no-op and getTranslation/<T> fall back to the global store. See docs/SSR.md.
31
+ }
32
+ })();
33
+ }
34
+ await alsInit;
35
+ }
36
+ /**
37
+ * Runs `fn` with a per-request translation scope active. Every `getTranslation(...)`
38
+ * call and every `<I18nKeylessText>` rendered within `fn` — synchronously OR across
39
+ * `await`s/streaming — resolves in `scope.lang` using `scope.translations`, with no
40
+ * change to the global store and full isolation between concurrent requests.
41
+ *
42
+ * Wrap your server render in it and await the result:
43
+ *
44
+ * const html = await runWithI18nKeyless(
45
+ * { lang, translations },
46
+ * () => renderToString(<App />)
47
+ * );
48
+ *
49
+ * Server-only. In the browser (or where AsyncLocalStorage is unavailable) it simply
50
+ * calls `fn` with no scoping. See docs/SSR.md.
51
+ */
52
+ export async function runWithI18nKeyless(scope, fn) {
53
+ await ensureALS();
54
+ return als ? als.run(scope, fn) : fn();
55
+ }
56
+ /**
57
+ * Returns the active per-request scope, or `undefined` when none is set (browser, or
58
+ * outside `runWithI18nKeyless`). Read internally by `getTranslation` and
59
+ * `<I18nKeylessText>`; exported for advanced/server use.
60
+ */
61
+ export function getRequestScope() {
62
+ return als?.getStore();
63
+ }
@@ -0,0 +1,18 @@
1
+ import { type Lang, type Translations } from "i18n-keyless-core";
2
+ /**
3
+ * Fetches the translations map for `lang` on the server, cached per process.
4
+ *
5
+ * Pass the result to `<I18nKeylessProvider lang={lang} translations={...}>` and
6
+ * serialize it into the HTML so the client can hydrate without a flash.
7
+ *
8
+ * Requires `init()` to have been called first (so the store holds API config). Returns
9
+ * an empty map for the primary language (no translation needed) and on fetch failure.
10
+ *
11
+ * See docs/SSR.md.
12
+ */
13
+ export declare function getServerTranslations(lang: Lang): Promise<Translations>;
14
+ /**
15
+ * Clears the per-process server translations cache. Pass a `lang` to evict a single
16
+ * language, or omit it to clear everything (e.g. after publishing new translations).
17
+ */
18
+ export declare function clearServerTranslationsCache(lang?: Lang): void;
package/dist/server.js ADDED
@@ -0,0 +1,49 @@
1
+ import { getAllTranslationsFromLanguage } from "i18n-keyless-core";
2
+ import { useI18nKeyless } from "./store.js";
3
+ /**
4
+ * Process-wide cache of translations per language.
5
+ *
6
+ * Translations for a given language are identical for every request, so they are
7
+ * global, cacheable data — only the *choice* of language is per-request. Caching here
8
+ * means each language is fetched at most once per server process. A process restart /
9
+ * redeploy naturally picks up new translations.
10
+ */
11
+ const cache = new Map();
12
+ /**
13
+ * Fetches the translations map for `lang` on the server, cached per process.
14
+ *
15
+ * Pass the result to `<I18nKeylessProvider lang={lang} translations={...}>` and
16
+ * serialize it into the HTML so the client can hydrate without a flash.
17
+ *
18
+ * Requires `init()` to have been called first (so the store holds API config). Returns
19
+ * an empty map for the primary language (no translation needed) and on fetch failure.
20
+ *
21
+ * See docs/SSR.md.
22
+ */
23
+ export async function getServerTranslations(lang) {
24
+ const store = useI18nKeyless.getState();
25
+ if (lang === store.config.languages.primary) {
26
+ return {};
27
+ }
28
+ const cached = cache.get(lang);
29
+ if (cached) {
30
+ return cached;
31
+ }
32
+ // lastRefresh: null forces a full fetch of the language.
33
+ const response = await getAllTranslationsFromLanguage(lang, { ...store, lastRefresh: null });
34
+ const translations = response?.ok ? response.data.translations : {};
35
+ cache.set(lang, translations);
36
+ return translations;
37
+ }
38
+ /**
39
+ * Clears the per-process server translations cache. Pass a `lang` to evict a single
40
+ * language, or omit it to clear everything (e.g. after publishing new translations).
41
+ */
42
+ export function clearServerTranslationsCache(lang) {
43
+ if (lang) {
44
+ cache.delete(lang);
45
+ }
46
+ else {
47
+ cache.clear();
48
+ }
49
+ }
package/dist/store.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Lang, type TranslationOptions } from "i18n-keyless-core";
2
- import { type I18nConfig, type TranslationStore } from "./types";
2
+ import { type I18nConfig, type TranslationStore } from "./types.ts";
3
3
  export declare const useI18nKeyless: import("zustand").UseBoundStore<import("zustand").StoreApi<TranslationStore>>;
4
4
  /**
5
5
  * Initializes the i18n configuration with defaults and validation
package/dist/store.js CHANGED
@@ -1,6 +1,15 @@
1
1
  import { queue, getAllTranslationsFromLanguage, getTranslationCore, sendTranslationsUsageToI18nKeyless, } from "i18n-keyless-core";
2
2
  import { create } from "zustand";
3
- import { storeKeys, setItem, getItem, clearI18nKeylessStorage, validateLanguage } from "./utils";
3
+ import { storeKeys, setItem, getItem, clearI18nKeylessStorage, validateLanguage, createMemoryStorage } from "./utils.js";
4
+ import { getRequestScope } from "./request-scope.js";
5
+ /**
6
+ * True when running without a DOM (server-side rendering). On the server the lib is
7
+ * read-only: usage analytics are neither recorded nor sent. Evaluated at call time so
8
+ * the environment can be detected (and stubbed in tests) per call. See docs/SSR.md.
9
+ */
10
+ function isServerEnv() {
11
+ return typeof window === "undefined";
12
+ }
4
13
  queue.on("empty", () => {
5
14
  // when each word is translated, fetch the translations for the current language
6
15
  const store = useI18nKeyless.getState();
@@ -190,8 +199,16 @@ export async function init(newConfig) {
190
199
  newConfig.languages.supported.push(newConfig.languages.initWithDefault);
191
200
  }
192
201
  if (!newConfig.storage) {
193
- console.log("storage is required", newConfig.storage);
194
- throw new Error("i18n-keyless: storage is required. You can use react-native-mmkv, @react-native-async-storage/async-storage, or window.localStorage, or any storage that has a getItem, setItem, removeItem, or get, set, and remove method");
202
+ if (typeof window === "undefined") {
203
+ // Server-side (SSR): no DOM storage exists. Default to an in-memory adapter so
204
+ // the server can init and cache translations for the process lifetime, instead
205
+ // of throwing. See docs/SSR.md.
206
+ newConfig.storage = createMemoryStorage();
207
+ }
208
+ else {
209
+ console.log("storage is required", newConfig.storage);
210
+ throw new Error("i18n-keyless: storage is required. You can use react-native-mmkv, @react-native-async-storage/async-storage, or window.localStorage, or any storage that has a getItem, setItem, removeItem, or get, set, and remove method");
211
+ }
195
212
  }
196
213
  if (!newConfig.getAllTranslations || !newConfig.handleTranslate) {
197
214
  if (!newConfig.API_KEY) {
@@ -213,7 +230,11 @@ export async function init(newConfig) {
213
230
  newConfig.onInit?.(currentLanguage);
214
231
  // initialize the language to fetch all the translations
215
232
  useI18nKeyless.getState().setLanguage(currentLanguage);
216
- useI18nKeyless.getState().sendTranslationsUsage();
233
+ // Read-only on the server: don't POST usage stats on boot (crawler-triggered renders
234
+ // and serverless per-request inits would otherwise pollute/spam the prune signal).
235
+ if (!isServerEnv() && !newConfig.ssr) {
236
+ useI18nKeyless.getState().sendTranslationsUsage();
237
+ }
217
238
  }
218
239
  export function useCurrentLanguage() {
219
240
  const currentLanguage = useI18nKeyless((state) => state.currentLanguage);
@@ -223,8 +244,17 @@ export function getSupportedLanguages() {
223
244
  return useI18nKeyless.getState().config.languages.supported;
224
245
  }
225
246
  export function getTranslation(key, options) {
226
- const store = useI18nKeyless.getState();
227
- store.setTranslationUsage(key, options?.context);
247
+ const base = useI18nKeyless.getState();
248
+ // Read-only on the server: don't record usage (a render may be a crawler hit).
249
+ if (!isServerEnv() && !base.config.ssr) {
250
+ base.setTranslationUsage(key, options?.context);
251
+ }
252
+ // SSR: if a per-request scope is active (set by runWithI18nKeyless), translate against
253
+ // that request's language/translations instead of the process-global store — so
254
+ // getTranslation, like <T>, renders the right language without leaking across
255
+ // concurrent requests. No scope (SPA / outside a scoped render) → use the store as-is.
256
+ const scope = getRequestScope();
257
+ const store = scope ? { ...base, currentLanguage: scope.lang, translations: scope.translations } : base;
228
258
  return getTranslationCore(key, store, options);
229
259
  }
230
260
  export function setCurrentLanguage(lang) {
package/dist/types.d.ts CHANGED
@@ -48,6 +48,18 @@ export interface I18nConfig {
48
48
  * if true, all the logs will be displayed in the console
49
49
  */
50
50
  debug?: boolean;
51
+ /**
52
+ * Read-only mode for server-side rendering.
53
+ *
54
+ * When the lib runs on a server (no `window`) it is automatically treated as
55
+ * read-only: usage analytics are not sent and not recorded (a server render can be
56
+ * triggered by a crawler, which would pollute the prune signal, and serverless
57
+ * per-request inits would POST on every request). Set `ssr: true` to force this
58
+ * read-only behavior explicitly even in an environment where `window` exists.
59
+ *
60
+ * This does NOT affect translate-on-miss — missing keys are still requested. See docs/SSR.md.
61
+ */
62
+ ssr?: boolean;
51
63
  /**
52
64
  * called everytime the language is set, maybe to set also the locale to dayjs or whatever
53
65
  */
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { I18nConfig } from "./types";
1
+ import type { I18nConfig } from "./types.ts";
2
2
  /**
3
3
  * The keys used to store i18n-keyless data in storage
4
4
  */
@@ -37,6 +37,19 @@ export declare function deleteItem(key: string, storage: I18nConfig["storage"]):
37
37
  * @param storage - The storage implementation to clear
38
38
  */
39
39
  export declare function clearI18nKeylessStorage(storage: I18nConfig["storage"]): Promise<void>;
40
+ /**
41
+ * Creates an in-memory storage adapter backed by a Map.
42
+ *
43
+ * Used as the default storage on the server (when `typeof window === "undefined"`
44
+ * and no `storage` is provided to `init`). It satisfies the storage interface and
45
+ * keeps translations cached for the lifetime of the process, so a long-lived server
46
+ * fetches each language at most once per boot. It does NOT persist across restarts —
47
+ * which is the correct, expected behavior server-side.
48
+ *
49
+ * On the browser, a missing `storage` remains a loud error (a real misconfiguration),
50
+ * so this is intentionally not used as a client default.
51
+ */
52
+ export declare function createMemoryStorage(): NonNullable<I18nConfig["storage"]>;
40
53
  /**
41
54
  * Validates the language against the supported languages
42
55
  * @param lang - The language to validate
package/dist/utils.js CHANGED
@@ -100,6 +100,33 @@ export async function clearI18nKeylessStorage(storage) {
100
100
  deleteItem(key, storage);
101
101
  }
102
102
  }
103
+ /**
104
+ * Creates an in-memory storage adapter backed by a Map.
105
+ *
106
+ * Used as the default storage on the server (when `typeof window === "undefined"`
107
+ * and no `storage` is provided to `init`). It satisfies the storage interface and
108
+ * keeps translations cached for the lifetime of the process, so a long-lived server
109
+ * fetches each language at most once per boot. It does NOT persist across restarts —
110
+ * which is the correct, expected behavior server-side.
111
+ *
112
+ * On the browser, a missing `storage` remains a loud error (a real misconfiguration),
113
+ * so this is intentionally not used as a client default.
114
+ */
115
+ export function createMemoryStorage() {
116
+ const map = new Map();
117
+ return {
118
+ getItem: (key) => map.get(key) ?? null,
119
+ setItem: (key, value) => {
120
+ map.set(key, value);
121
+ },
122
+ removeItem: (key) => {
123
+ map.delete(key);
124
+ },
125
+ clear: () => {
126
+ map.clear();
127
+ },
128
+ };
129
+ }
103
130
  /**
104
131
  * Validates the language against the supported languages
105
132
  * @param lang - The language to validate
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "i18n-keyless-react",
3
3
  "private": false,
4
- "version": "1.18.0",
4
+ "version": "2.1.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -24,7 +24,7 @@
24
24
  "postpublish": "rm -rf ./dist && rm *.tgz"
25
25
  },
26
26
  "dependencies": {
27
- "i18n-keyless-core": "1.18.0",
27
+ "i18n-keyless-core": "2.1.0",
28
28
  "zustand": "^5.0.3"
29
29
  },
30
30
  "peerDependencies": {