i18n-keyless-react 2.1.0 → 2.2.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/index.d.ts CHANGED
@@ -1,5 +1,5 @@
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";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
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";
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
@@ -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
- const translations = await getItem(storeKeys.translations, storage, JSON.parse);
134
- if (translations) {
135
- if (debug)
136
- console.log("i18n-keyless: _hydrate", translations);
137
- useI18nKeyless.setState({ translations: translations });
138
- }
139
- else {
140
- if (debug)
141
- console.log("i18n-keyless: _hydrate: no translations");
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 (skipCurrentLanguageHydration) {
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,8 +287,12 @@ 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
  }
252
297
  // SSR: if a per-request scope is active (set by runWithI18nKeyless), translate against
253
298
  // that request's language/translations instead of the process-global store — so
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "i18n-keyless-react",
3
3
  "private": false,
4
- "version": "2.1.0",
4
+ "version": "2.2.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.1.0",
27
+ "i18n-keyless-core": "2.2.0",
28
28
  "zustand": "^5.0.3"
29
29
  },
30
30
  "peerDependencies": {