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.
- package/dist/I18nKeylessProvider.d.ts +48 -0
- package/dist/I18nKeylessProvider.js +38 -0
- package/dist/I18nKeylessText.js +12 -3
- package/dist/index.d.ts +11 -5
- package/dist/index.js +7 -3
- package/dist/request-scope.d.ts +36 -0
- package/dist/request-scope.js +63 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.js +49 -0
- package/dist/store.d.ts +1 -1
- package/dist/store.js +36 -6
- package/dist/types.d.ts +12 -0
- package/dist/utils.d.ts +14 -1
- package/dist/utils.js +27 -0
- package/package.json +2 -2
|
@@ -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
|
+
};
|
package/dist/I18nKeylessText.js
CHANGED
|
@@ -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
|
|
12
|
-
const
|
|
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
|
|
5
|
-
export
|
|
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
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
|
227
|
-
|
|
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.
|
|
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.
|
|
27
|
+
"i18n-keyless-core": "2.1.0",
|
|
28
28
|
"zustand": "^5.0.3"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|