i18n-keyless-react 1.18.0 → 2.0.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,7 @@
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";
4
5
  const warnAboutWhitespace = (text) => {
5
6
  if (process.env.NODE_ENV === "development" && text !== text.trim()) {
6
7
  console.warn(`I18nKeylessText received text with leading/trailing whitespace: "${text}". ` +
@@ -8,9 +9,15 @@ const warnAboutWhitespace = (text) => {
8
9
  }
9
10
  };
10
11
  export const I18nKeylessText = ({ children, replace, context, debug = false, forceTemporary }) => {
11
- const translations = useI18nKeyless((store) => store.translations);
12
- const currentLanguage = useI18nKeyless((store) => store.currentLanguage);
12
+ const storeTranslations = useI18nKeyless((store) => store.translations);
13
+ const storeCurrentLanguage = useI18nKeyless((store) => store.currentLanguage);
13
14
  const config = useI18nKeyless((store) => store.config);
15
+ // In SSR/provider mode, the language and translations come from the per-request
16
+ // context (so concurrent requests don't share state). Otherwise (SPA mode) they come
17
+ // from the global store. See docs/SSR.md.
18
+ const providerContext = useI18nKeylessContext();
19
+ const translations = providerContext?.translations ?? storeTranslations;
20
+ const currentLanguage = providerContext?.lang ?? storeCurrentLanguage;
14
21
  // Trim the source text immediately
15
22
  const rawText = Array.isArray(children) ? children.join("") : String(children ?? "");
16
23
  const sourceText = rawText.trim();
package/dist/index.d.ts CHANGED
@@ -1,6 +1,10 @@
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 { clearI18nKeylessStorageAndStore } from "./store.ts";
8
+ export type { I18nKeylessTextProps } from "./I18nKeylessText.tsx";
9
+ export { type I18nConfig, type TranslationStoreState, type TranslationOptions, type TranslationStore } from "./types.ts";
6
10
  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,7 @@
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 { clearI18nKeylessStorageAndStore } from "./store.js";
4
7
  export { AVAILABLE_LANGS, getAllTranslationsFromLanguage, queue } from "i18n-keyless-core";
@@ -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,14 @@
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
+ /**
5
+ * True when running without a DOM (server-side rendering). On the server the lib is
6
+ * read-only: usage analytics are neither recorded nor sent. Evaluated at call time so
7
+ * the environment can be detected (and stubbed in tests) per call. See docs/SSR.md.
8
+ */
9
+ function isServerEnv() {
10
+ return typeof window === "undefined";
11
+ }
4
12
  queue.on("empty", () => {
5
13
  // when each word is translated, fetch the translations for the current language
6
14
  const store = useI18nKeyless.getState();
@@ -190,8 +198,16 @@ export async function init(newConfig) {
190
198
  newConfig.languages.supported.push(newConfig.languages.initWithDefault);
191
199
  }
192
200
  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");
201
+ if (typeof window === "undefined") {
202
+ // Server-side (SSR): no DOM storage exists. Default to an in-memory adapter so
203
+ // the server can init and cache translations for the process lifetime, instead
204
+ // of throwing. See docs/SSR.md.
205
+ newConfig.storage = createMemoryStorage();
206
+ }
207
+ else {
208
+ console.log("storage is required", newConfig.storage);
209
+ 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");
210
+ }
195
211
  }
196
212
  if (!newConfig.getAllTranslations || !newConfig.handleTranslate) {
197
213
  if (!newConfig.API_KEY) {
@@ -213,7 +229,11 @@ export async function init(newConfig) {
213
229
  newConfig.onInit?.(currentLanguage);
214
230
  // initialize the language to fetch all the translations
215
231
  useI18nKeyless.getState().setLanguage(currentLanguage);
216
- useI18nKeyless.getState().sendTranslationsUsage();
232
+ // Read-only on the server: don't POST usage stats on boot (crawler-triggered renders
233
+ // and serverless per-request inits would otherwise pollute/spam the prune signal).
234
+ if (!isServerEnv() && !newConfig.ssr) {
235
+ useI18nKeyless.getState().sendTranslationsUsage();
236
+ }
217
237
  }
218
238
  export function useCurrentLanguage() {
219
239
  const currentLanguage = useI18nKeyless((state) => state.currentLanguage);
@@ -224,7 +244,10 @@ export function getSupportedLanguages() {
224
244
  }
225
245
  export function getTranslation(key, options) {
226
246
  const store = useI18nKeyless.getState();
227
- store.setTranslationUsage(key, options?.context);
247
+ // Read-only on the server: don't record usage (a render may be a crawler hit).
248
+ if (!isServerEnv() && !store.config.ssr) {
249
+ store.setTranslationUsage(key, options?.context);
250
+ }
228
251
  return getTranslationCore(key, store, options);
229
252
  }
230
253
  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.0.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.0.0",
28
28
  "zustand": "^5.0.3"
29
29
  },
30
30
  "peerDependencies": {