i18n-keyless-react 2.1.0 → 2.3.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/I18nKeylessText.js +6 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/request-scope.d.ts +22 -3
- package/dist/request-scope.js +43 -5
- package/dist/store.d.ts +16 -1
- package/dist/store.js +60 -12
- package/package.json +2 -2
package/dist/I18nKeylessText.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import React, { useEffect, useMemo } from "react";
|
|
3
3
|
import { useI18nKeyless, getTranslation } from "./store.js";
|
|
4
4
|
import { useI18nKeylessContext } from "./I18nKeylessProvider.js";
|
|
5
|
-
import { getRequestScope } from "./request-scope.js";
|
|
5
|
+
import { getRequestScope, recordUsedKey } from "./request-scope.js";
|
|
6
6
|
const warnAboutWhitespace = (text) => {
|
|
7
7
|
if (process.env.NODE_ENV === "development" && text !== text.trim()) {
|
|
8
8
|
console.warn(`I18nKeylessText received text with leading/trailing whitespace: "${text}". ` +
|
|
@@ -29,9 +29,11 @@ export const I18nKeylessText = ({ children, replace, context, debug = false, for
|
|
|
29
29
|
useEffect(() => {
|
|
30
30
|
getTranslation(sourceText, { context, debug, forceTemporary });
|
|
31
31
|
}, [sourceText, currentLanguage, context, debug, forceTemporary]);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
const storageKey = context ? `${sourceText}__${context}` : sourceText;
|
|
33
|
+
// Record the key for the per-page SSR snapshot (no-op off-server; pure Set.add, no
|
|
34
|
+
// setState, so no render-time update warning). See docs/SSR.md.
|
|
35
|
+
recordUsedKey(storageKey);
|
|
36
|
+
const translatedText = currentLanguage === config.languages.primary ? sourceText : translations[storageKey] || sourceText;
|
|
35
37
|
const finalText = useMemo(() => {
|
|
36
38
|
if (!replace) {
|
|
37
39
|
return translatedText;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export { I18nKeylessText, I18nKeylessText as T } from "./I18nKeylessText.tsx";
|
|
2
|
-
export { init, setCurrentLanguage, useCurrentLanguage, getTranslation, getSupportedLanguages, useI18nKeyless } from "./store.ts";
|
|
2
|
+
export { init, hydrateFromServer, setCurrentLanguage, useCurrentLanguage, getTranslation, getSupportedLanguages, useI18nKeyless } from "./store.ts";
|
|
3
3
|
export { clearI18nKeylessStorage, validateLanguage, createMemoryStorage } from "./utils.ts";
|
|
4
4
|
export { I18nKeylessProvider, useI18nKeylessContext } from "./I18nKeylessProvider.tsx";
|
|
5
5
|
export type { I18nKeylessProviderProps } from "./I18nKeylessProvider.tsx";
|
|
6
6
|
export { getServerTranslations, clearServerTranslationsCache } from "./server.ts";
|
|
7
|
-
export { runWithI18nKeyless, getRequestScope } from "./request-scope.ts";
|
|
7
|
+
export { runWithI18nKeyless, getRequestScope, getUsedTranslationsSnapshot } from "./request-scope.ts";
|
|
8
8
|
export type { I18nRequestScope } from "./request-scope.ts";
|
|
9
9
|
export { clearI18nKeylessStorageAndStore } from "./store.ts";
|
|
10
10
|
export type { I18nKeylessTextProps } from "./I18nKeylessText.tsx";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { I18nKeylessText, I18nKeylessText as T } from "./I18nKeylessText.js";
|
|
2
|
-
export { init, setCurrentLanguage, useCurrentLanguage, getTranslation, getSupportedLanguages, useI18nKeyless } from "./store.js";
|
|
2
|
+
export { init, hydrateFromServer, setCurrentLanguage, useCurrentLanguage, getTranslation, getSupportedLanguages, useI18nKeyless } from "./store.js";
|
|
3
3
|
export { clearI18nKeylessStorage, validateLanguage, createMemoryStorage } from "./utils.js";
|
|
4
4
|
export { I18nKeylessProvider, useI18nKeylessContext } from "./I18nKeylessProvider.js";
|
|
5
5
|
export { getServerTranslations, clearServerTranslationsCache } from "./server.js";
|
|
6
|
-
export { runWithI18nKeyless, getRequestScope } from "./request-scope.js";
|
|
6
|
+
export { runWithI18nKeyless, getRequestScope, getUsedTranslationsSnapshot } from "./request-scope.js";
|
|
7
7
|
export { clearI18nKeylessStorageAndStore } from "./store.js";
|
|
8
8
|
export { AVAILABLE_LANGS, getAllTranslationsFromLanguage, queue } from "i18n-keyless-core";
|
package/dist/request-scope.d.ts
CHANGED
|
@@ -29,8 +29,27 @@ export interface I18nRequestScope {
|
|
|
29
29
|
*/
|
|
30
30
|
export declare function runWithI18nKeyless<R>(scope: I18nRequestScope, fn: () => R): Promise<R>;
|
|
31
31
|
/**
|
|
32
|
-
* Returns the active per-request scope,
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* Returns the active per-request scope (`{ lang, translations }` with the FULL set
|
|
33
|
+
* available for resolution), or `undefined` when none is set (browser, or outside
|
|
34
|
+
* `runWithI18nKeyless`). Read internally by `getTranslation` and `<I18nKeylessText>`;
|
|
35
|
+
* exported for advanced/server use.
|
|
35
36
|
*/
|
|
36
37
|
export declare function getRequestScope(): I18nRequestScope | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Records that `key` (the storage key, i.e. `key` or `key__context`) was used in the
|
|
40
|
+
* current render. No-op outside a `runWithI18nKeyless` scope (browser / SPA). Pure
|
|
41
|
+
* `Set.add` — no store write, no setState. Internal: called by `getTranslation` and
|
|
42
|
+
* `<I18nKeylessText>`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function recordUsedKey(key: string): void;
|
|
45
|
+
/**
|
|
46
|
+
* Returns a snapshot containing ONLY the keys used during the current render (∩ the keys
|
|
47
|
+
* actually available in the scope's translations), for serializing a per-page subset into
|
|
48
|
+
* the SSR HTML instead of the full language set. Use it at the serialization site in
|
|
49
|
+
* place of `getRequestScope()` when the language set is large.
|
|
50
|
+
*
|
|
51
|
+
* The full set is still used for resolution during render; only the serialized snapshot is
|
|
52
|
+
* narrowed. Pair it with `init()`'s background full fetch on the client so subsequent
|
|
53
|
+
* client-side navigation has every key. Returns `undefined` outside a scope. See docs/SSR.md.
|
|
54
|
+
*/
|
|
55
|
+
export declare function getUsedTranslationsSnapshot(): I18nRequestScope | undefined;
|
package/dist/request-scope.js
CHANGED
|
@@ -51,13 +51,51 @@ async function ensureALS() {
|
|
|
51
51
|
*/
|
|
52
52
|
export async function runWithI18nKeyless(scope, fn) {
|
|
53
53
|
await ensureALS();
|
|
54
|
-
|
|
54
|
+
if (!als) {
|
|
55
|
+
return fn();
|
|
56
|
+
}
|
|
57
|
+
// Each run gets its own `used` Set, isolated to this request's async context.
|
|
58
|
+
return als.run({ lang: scope.lang, translations: scope.translations, used: new Set() }, fn);
|
|
55
59
|
}
|
|
56
60
|
/**
|
|
57
|
-
* Returns the active per-request scope,
|
|
58
|
-
*
|
|
59
|
-
*
|
|
61
|
+
* Returns the active per-request scope (`{ lang, translations }` with the FULL set
|
|
62
|
+
* available for resolution), or `undefined` when none is set (browser, or outside
|
|
63
|
+
* `runWithI18nKeyless`). Read internally by `getTranslation` and `<I18nKeylessText>`;
|
|
64
|
+
* exported for advanced/server use.
|
|
60
65
|
*/
|
|
61
66
|
export function getRequestScope() {
|
|
62
|
-
|
|
67
|
+
const s = als?.getStore();
|
|
68
|
+
return s ? { lang: s.lang, translations: s.translations } : undefined;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Records that `key` (the storage key, i.e. `key` or `key__context`) was used in the
|
|
72
|
+
* current render. No-op outside a `runWithI18nKeyless` scope (browser / SPA). Pure
|
|
73
|
+
* `Set.add` — no store write, no setState. Internal: called by `getTranslation` and
|
|
74
|
+
* `<I18nKeylessText>`.
|
|
75
|
+
*/
|
|
76
|
+
export function recordUsedKey(key) {
|
|
77
|
+
als?.getStore()?.used.add(key);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Returns a snapshot containing ONLY the keys used during the current render (∩ the keys
|
|
81
|
+
* actually available in the scope's translations), for serializing a per-page subset into
|
|
82
|
+
* the SSR HTML instead of the full language set. Use it at the serialization site in
|
|
83
|
+
* place of `getRequestScope()` when the language set is large.
|
|
84
|
+
*
|
|
85
|
+
* The full set is still used for resolution during render; only the serialized snapshot is
|
|
86
|
+
* narrowed. Pair it with `init()`'s background full fetch on the client so subsequent
|
|
87
|
+
* client-side navigation has every key. Returns `undefined` outside a scope. See docs/SSR.md.
|
|
88
|
+
*/
|
|
89
|
+
export function getUsedTranslationsSnapshot() {
|
|
90
|
+
const s = als?.getStore();
|
|
91
|
+
if (!s) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
const translations = {};
|
|
95
|
+
for (const key of s.used) {
|
|
96
|
+
if (key in s.translations) {
|
|
97
|
+
translations[key] = s.translations[key];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { lang: s.lang, translations };
|
|
63
101
|
}
|
package/dist/store.d.ts
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
import { type Lang, type TranslationOptions } from "i18n-keyless-core";
|
|
1
|
+
import { type Lang, type Translations, type TranslationOptions } from "i18n-keyless-core";
|
|
2
2
|
import { type I18nConfig, type TranslationStore } from "./types.ts";
|
|
3
3
|
export declare const useI18nKeyless: import("zustand").UseBoundStore<import("zustand").StoreApi<TranslationStore>>;
|
|
4
|
+
/**
|
|
5
|
+
* Synchronously seeds the store from a server snapshot, BEFORE React's first client
|
|
6
|
+
* render, so the imperative `getTranslation(key)` returns the correct language on the
|
|
7
|
+
* very first render — no hydration mismatch, no blink. Call it in your client entry,
|
|
8
|
+
* before `hydrateRoot`, with the `{ lang, translations }` the server serialized into the
|
|
9
|
+
* HTML (the server can read it from `getRequestScope()`).
|
|
10
|
+
*
|
|
11
|
+
* The component path (`<I18nKeylessText>`) is covered by `<I18nKeylessProvider>`; the
|
|
12
|
+
* function path needs this synchronous store seed because a plain function cannot read
|
|
13
|
+
* React context. Server-only async `hydrate()` will not overwrite this seed. See docs/SSR.md.
|
|
14
|
+
*/
|
|
15
|
+
export declare function hydrateFromServer(snapshot?: {
|
|
16
|
+
lang?: Lang;
|
|
17
|
+
translations?: Translations;
|
|
18
|
+
}): void;
|
|
4
19
|
/**
|
|
5
20
|
* Initializes the i18n configuration with defaults and validation
|
|
6
21
|
* @param newConfig - The configuration object to initialize
|
package/dist/store.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { queue, getAllTranslationsFromLanguage, getTranslationCore, sendTranslationsUsageToI18nKeyless, } from "i18n-keyless-core";
|
|
2
2
|
import { create } from "zustand";
|
|
3
3
|
import { storeKeys, setItem, getItem, clearI18nKeylessStorage, validateLanguage, createMemoryStorage } from "./utils.js";
|
|
4
|
-
import { getRequestScope } from "./request-scope.js";
|
|
4
|
+
import { getRequestScope, recordUsedKey } from "./request-scope.js";
|
|
5
5
|
/**
|
|
6
6
|
* True when running without a DOM (server-side rendering). On the server the lib is
|
|
7
7
|
* read-only: usage analytics are neither recorded nor sent. Evaluated at call time so
|
|
@@ -10,6 +10,14 @@ import { getRequestScope } from "./request-scope.js";
|
|
|
10
10
|
function isServerEnv() {
|
|
11
11
|
return typeof window === "undefined";
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* True once a server snapshot has been applied synchronously on the client (via
|
|
15
|
+
* `hydrateFromServer`). When set, the async `hydrate()` treats the snapshot as
|
|
16
|
+
* authoritative for the current request and does not overwrite the seeded language /
|
|
17
|
+
* translations from storage (which may hold a different, stale language). Never set in
|
|
18
|
+
* SPA mode, so SPA hydration is unchanged. See docs/SSR.md.
|
|
19
|
+
*/
|
|
20
|
+
let serverSnapshotApplied = false;
|
|
13
21
|
queue.on("empty", () => {
|
|
14
22
|
// when each word is translated, fetch the translations for the current language
|
|
15
23
|
const store = useI18nKeyless.getState();
|
|
@@ -120,6 +128,28 @@ export const useI18nKeyless = create((set, get) => ({
|
|
|
120
128
|
}
|
|
121
129
|
},
|
|
122
130
|
}));
|
|
131
|
+
/**
|
|
132
|
+
* Synchronously seeds the store from a server snapshot, BEFORE React's first client
|
|
133
|
+
* render, so the imperative `getTranslation(key)` returns the correct language on the
|
|
134
|
+
* very first render — no hydration mismatch, no blink. Call it in your client entry,
|
|
135
|
+
* before `hydrateRoot`, with the `{ lang, translations }` the server serialized into the
|
|
136
|
+
* HTML (the server can read it from `getRequestScope()`).
|
|
137
|
+
*
|
|
138
|
+
* The component path (`<I18nKeylessText>`) is covered by `<I18nKeylessProvider>`; the
|
|
139
|
+
* function path needs this synchronous store seed because a plain function cannot read
|
|
140
|
+
* React context. Server-only async `hydrate()` will not overwrite this seed. See docs/SSR.md.
|
|
141
|
+
*/
|
|
142
|
+
export function hydrateFromServer(snapshot) {
|
|
143
|
+
if (!snapshot?.lang) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
serverSnapshotApplied = true;
|
|
147
|
+
const current = useI18nKeyless.getState();
|
|
148
|
+
useI18nKeyless.setState({
|
|
149
|
+
currentLanguage: snapshot.lang,
|
|
150
|
+
translations: { ...current.translations, ...(snapshot.translations ?? {}) },
|
|
151
|
+
});
|
|
152
|
+
}
|
|
123
153
|
async function hydrate() {
|
|
124
154
|
const config = useI18nKeyless.getState().config;
|
|
125
155
|
if (!config.API_KEY) {
|
|
@@ -130,15 +160,21 @@ async function hydrate() {
|
|
|
130
160
|
if (!storage) {
|
|
131
161
|
throw new Error(`i18n-keyless: storage is not initialized hydrating`);
|
|
132
162
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
163
|
+
// When a server snapshot was applied, it is authoritative for the current request's
|
|
164
|
+
// language: skip loading translations / currentLanguage from storage (storage holds a
|
|
165
|
+
// single, possibly different/stale language and would clobber or mix the seed). Usage,
|
|
166
|
+
// uniqueId and lastRefresh are language-independent and still hydrate normally.
|
|
167
|
+
if (!serverSnapshotApplied) {
|
|
168
|
+
const translations = await getItem(storeKeys.translations, storage, JSON.parse);
|
|
169
|
+
if (translations) {
|
|
170
|
+
if (debug)
|
|
171
|
+
console.log("i18n-keyless: _hydrate", translations);
|
|
172
|
+
useI18nKeyless.setState({ translations: translations });
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
if (debug)
|
|
176
|
+
console.log("i18n-keyless: _hydrate: no translations");
|
|
177
|
+
}
|
|
142
178
|
}
|
|
143
179
|
const translationsUsage = await getItem(storeKeys.translationsUsage, storage, JSON.parse);
|
|
144
180
|
if (translationsUsage) {
|
|
@@ -152,7 +188,12 @@ async function hydrate() {
|
|
|
152
188
|
}
|
|
153
189
|
const currentLanguage = await getItem(storeKeys.currentLanguage, storage);
|
|
154
190
|
const skipCurrentLanguageHydration = config.languages.skipCurrentLanguageHydration;
|
|
155
|
-
if (
|
|
191
|
+
if (serverSnapshotApplied) {
|
|
192
|
+
// keep the synchronously-seeded language; do not override it from storage
|
|
193
|
+
if (debug)
|
|
194
|
+
console.log("i18n-keyless: _hydrate: keeping server-seeded language");
|
|
195
|
+
}
|
|
196
|
+
else if (skipCurrentLanguageHydration) {
|
|
156
197
|
if (debug)
|
|
157
198
|
console.log("i18n-keyless: _hydrate: skip current language hydration");
|
|
158
199
|
useI18nKeyless.setState({ currentLanguage: config?.languages.initWithDefault });
|
|
@@ -246,9 +287,16 @@ export function getSupportedLanguages() {
|
|
|
246
287
|
export function getTranslation(key, options) {
|
|
247
288
|
const base = useI18nKeyless.getState();
|
|
248
289
|
// Read-only on the server: don't record usage (a render may be a crawler hit).
|
|
290
|
+
// On the client, DEFER the usage write: getTranslation is called during component
|
|
291
|
+
// render, and setTranslationUsage writes to the store synchronously, which makes React
|
|
292
|
+
// log "Cannot update a component while rendering a different component". Usage analytics
|
|
293
|
+
// never needs to affect the current render, so flush it on a microtask (after render).
|
|
249
294
|
if (!isServerEnv() && !base.config.ssr) {
|
|
250
|
-
base.setTranslationUsage(key, options?.context);
|
|
295
|
+
queueMicrotask(() => base.setTranslationUsage(key, options?.context));
|
|
251
296
|
}
|
|
297
|
+
// Record the key for the per-page SSR snapshot (no-op off-server). Pure Set.add — no
|
|
298
|
+
// store write. Use the storage key (with context) so it matches the translations map.
|
|
299
|
+
recordUsedKey(options?.context ? `${key}__${options.context}` : key);
|
|
252
300
|
// SSR: if a per-request scope is active (set by runWithI18nKeyless), translate against
|
|
253
301
|
// that request's language/translations instead of the process-global store — so
|
|
254
302
|
// getTranslation, like <T>, renders the right language without leaking across
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "i18n-keyless-react",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.3.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": "2.
|
|
27
|
+
"i18n-keyless-core": "2.3.0",
|
|
28
28
|
"zustand": "^5.0.3"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|