langsys-js-react 0.6.6 → 0.7.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,8 +1,8 @@
1
- import * as langsys_js_typescript from 'langsys-js-typescript';
2
- import { Signal, TFunction, iCategories, ParamPrimitive, iLangsysInitConfig as iLangsysInitConfig$1, iLangsysResponse, iCountryList, iCurrencyList, iCountryDialCode, iLocaleDefault, iLocaleFlat, iLocaleData } from 'langsys-js-typescript';
3
- export { ExtractParamKeys, LangsysAppAPI, ParamPrimitive, ParamsFor, Signal, TArgs, TFunction, TranslationParams, canonicalizeLocale, createSignal, currentlyLoadedLocale, iCategories, iContentBlock, iCountry, iCountryDialCode, iCountryList, iCurrency, iCurrencyList, iLangsysResponse, iLanguageName, iLocaleData, iLocaleDefault, iLocaleFlat, iProject, iTranslations, sTranslations, tSignal as t } from 'langsys-js-typescript';
1
+ import { Signal, ServerMessage, TFunction, iCategories, ParamPrimitive, RequestSeed, RequestScope } from 'langsys-js-typescript';
2
+ export { CatalogSnapshot, CategoryMoveOptions, CategoryRef, DEFAULT_SERVER_MESSAGE_CATEGORY, ExtractParamKeys, LangsysAppAPI, LangsysConfigurationError, LegacyFormatError, LegacyKeyFile, ParamPrimitive, ParamsFor, RegistrationReason, RegistrationResult, ResolveServerMessagesOptions, ServerMessage, ServerMessagePieces, Signal, SnapshotError, TArgs, TFunction, TranslationParams, WriteGrant, canonicalizeLocale, createSignal, currentlyLoadedLocale, iCategories, iContentBlock, iCountry, iCountryDialCode, iCountryList, iCurrency, iCurrencyList, iLangsysResponse, iLanguageName, iLocaleData, iLocaleDefault, iLocaleFlat, iProject, iTranslations, inCategory, interpolate, isCategoryRef, localeHeaders, notifyNavigation, renderServerMessage, resolveServerMessages, sTranslations, setWriteGrant, tSignal as t } from 'langsys-js-typescript';
4
3
  import * as react from 'react';
5
4
  import { ReactNode } from 'react';
5
+ export { L as LangsysApp, c as createLocaleStore, i as iLangsysInitConfig } from './app-zQKL7-vo.js';
6
6
 
7
7
  /**
8
8
  * Subscribe a React component to a base-SDK `Signal<T>` and return its current
@@ -11,9 +11,12 @@ import { ReactNode } from 'react';
11
11
  * This is the React mirror of Svelte's `$store` auto-subscription. Where the
12
12
  * Svelte wrapper adapts a native store *into* the SDK (`writable` → `Signal`),
13
13
  * the React wrapper adapts the SDK's signals *out* to the render cycle. It is
14
- * built on `useSyncExternalStore`, so it is concurrent-safe and SSR-safe — the
15
- * server snapshot reads the same `.get()`, which the base SDK seeds from
16
- * `initialTranslations` during SSR.
14
+ * built on `useSyncExternalStore`, so it is concurrent-safe: a render never
15
+ * observes two different values of the same signal (no tearing). On the
16
+ * server the snapshot reads the value for the current request: inside a core
17
+ * request scope the signals answer from the scope, and under a
18
+ * `<LangsysProvider seed>` the server snapshot — used for the server render and
19
+ * the hydration render — reads through the scope the provider rebuilt.
17
20
  *
18
21
  * The base SDK's signals are stable between changes (every `set` replaces the
19
22
  * value, so `.get()` returns a fresh reference only after a real change). That
@@ -26,21 +29,6 @@ import { ReactNode } from 'react';
26
29
  * every render would resubscribe on every render.
27
30
  */
28
31
  declare function useSignal<T>(signal: Signal<T>): T;
29
- /**
30
- * Create a reactive `Signal<string>` to hold the user's selected locale — the
31
- * React analog of Svelte's `writable('en-US')`.
32
- *
33
- * Pass the result as `UserLocaleStore` to `LangsysApp.init`, read it reactively
34
- * with `useSignal(store)` (or use the all-in-one `useLocaleStore` hook), and
35
- * switch locale with `store.set('fr-FR')`. The base SDK only ever reads and
36
- * subscribes to it — it never writes.
37
- *
38
- * Locale identifiers are canonicalized to BCP 47 by the base SDK (v0.3.0+), so
39
- * `'en-us'` still works on input — but `currentlyLoadedLocale` always emits the
40
- * canonical form (`'en-US'`), so prefer canonical casing to keep comparisons
41
- * against it straightforward.
42
- */
43
- declare function createLocaleStore(initial?: string): Signal<string>;
44
32
 
45
33
  /**
46
34
  * The current translation function, re-rendering the calling component whenever
@@ -82,6 +70,66 @@ declare function useTranslations(): iCategories;
82
70
  * safe to pass to `LangsysApp.init` and to depend on in effects.
83
71
  */
84
72
  declare function useLocaleStore(initial?: string): [string, (locale: string) => void, Signal<string>];
73
+ /**
74
+ * Whether the current session may register content, as decided by the server.
75
+ *
76
+ * Tri-state, and the three states are genuinely distinct:
77
+ * - `undefined` — authorization hasn't landed yet. Not the same as read-only.
78
+ * - `false` — read-only session; the SDK may report the page URL instead,
79
+ * subject to the key's auto-discovery permission. A read-only
80
+ * key with discovery disallowed reports nothing at all.
81
+ * - `true` — this session registers content directly.
82
+ *
83
+ * Don't collapse it to a boolean and don't default `undefined` to `false`: the
84
+ * same key can be write-enabled from one IP and read-only from another, so only
85
+ * the server can answer, and "not yet known" is a real state to render for.
86
+ *
87
+ * Unlike the other hooks this does *not* go through `useSignal`. `writeEnabled`
88
+ * is browser-authoritative — it is only ever written client-side and stays
89
+ * `undefined` during SSR. Reusing `useSignal` would pass `signal.get` as the
90
+ * server snapshot, and `getServerSnapshot` is used both on the server *and* for
91
+ * the hydration render on the client. If authorization resolved before
92
+ * hydration, that render would disagree with the server HTML and React would
93
+ * throw the markup away. Pinning the server snapshot to `undefined` keeps the
94
+ * hydration pass matching the server; the real value arrives right after.
95
+ */
96
+ declare function useWriteEnabled(): boolean | undefined;
97
+ /**
98
+ * Tell the SDK the route changed, whenever `location` changes. Call it once, anywhere under
99
+ * your router, with the router's current location:
100
+ *
101
+ * // React Router
102
+ * useNotifyNavigation(useLocation().key);
103
+ * // Next.js App Router
104
+ * useNotifyNavigation(usePathname() + '?' + useSearchParams());
105
+ *
106
+ * Why it exists: content that stays mounted across a route change — a persistent layout, a
107
+ * header — is never re-rendered by React when only the route changes, because React skips
108
+ * a subtree whose element is unchanged. Nothing then calls back into the SDK, so the new
109
+ * page is never credited with that content. `notifyNavigation()` makes every mounted
110
+ * translated node look itself up again at the new URL. This hook only times that call; the
111
+ * SDK decides everything else (spec HINT-13).
112
+ *
113
+ * This package cannot know which router an app uses, so it takes the router's location as a
114
+ * plain value instead of importing any router. Calling `notifyNavigation` directly from a
115
+ * router's own after-navigation hook is equivalent.
116
+ */
117
+ declare function useNotifyNavigation(location: unknown): void;
118
+ /**
119
+ * A function that renders server message entries, re-rendering the calling component whenever
120
+ * translations or the loaded locale change:
121
+ *
122
+ * const render = useRenderServerMessage();
123
+ * const entries = resolveServerMessages(responseBody);
124
+ * return <ul>{entries.map((e, i) => <li key={i}>{render(e)}</li>)}</ul>;
125
+ *
126
+ * It is the SDK's `renderServerMessage` and decides nothing itself: the entry's template is
127
+ * looked up under the messages category (`messagesCategory` in `init`, `Errors` by default) and
128
+ * filled from its params, and the entry's `message` is shown when the catalog has no
129
+ * translation for it (spec MSG-5). The returned function changes identity exactly when a
130
+ * lookup could give a different answer, so it can be depended on in memos and effects.
131
+ */
132
+ declare function useRenderServerMessage(): (entry: ServerMessage, category?: string) => string;
85
133
 
86
134
  /**
87
135
  * Props for the React `Translate` component. Mirrors the Svelte component's
@@ -90,6 +138,15 @@ declare function useLocaleStore(initial?: string): [string, (locale: string) =>
90
138
  interface TranslateProps {
91
139
  /** Optional category under which tokens are registered. Helps translators disambiguate. */
92
140
  category?: string;
141
+ /**
142
+ * The category this content was registered under before it moved to `category` (MOVE-1):
143
+ * `''` for none, absent for no move. Its translations follow it instead of being made, and
144
+ * charged for, again; until the catalog holds it under `category`, the previous category's
145
+ * entry renders and the host says so (MOVE-4, MOVE-5). Never part of its identity (MOVE-2),
146
+ * and sent only on the registration a miss triggers (MOVE-3), so once the catalog has it the
147
+ * prop does nothing and can be deleted whenever convenient.
148
+ */
149
+ previousCategory?: string | null;
93
150
  /** Optional stable id for the content block. If omitted, the SDK hashes category + tokens. */
94
151
  custom_id?: string;
95
152
  /** Optional human-readable label shown in the Translation Manager. */
@@ -100,6 +157,13 @@ interface TranslateProps {
100
157
  * `{key}` at capture). Applied to content-block text nodes, translatable
101
158
  * attributes, `<option>` text, and single-token content — including
102
159
  * untranslated fallbacks. Number/Date values get CLDR locale formatting.
160
+ *
161
+ * A bare `{key}` doesn't just break interpolation — JSX evaluates it before
162
+ * the walker runs, so the *value* is captured as part of the phrase and each
163
+ * distinct value hashes to its own content block. A `<Translate>` around a
164
+ * live counter registers a new block per tick while rendering correctly in
165
+ * the base locale the whole time. `%key%` yields one stable id for all
166
+ * values.
103
167
  */
104
168
  params?: Record<string, ParamPrimitive>;
105
169
  /** Host element tag. Defaults to a `<translate>` custom element. */
@@ -107,24 +171,30 @@ interface TranslateProps {
107
171
  /** Class applied to the host element. */
108
172
  className?: string;
109
173
  children?: ReactNode;
174
+ /**
175
+ * Set by the build transform when the block would store a value it left as written (inside
176
+ * MathML or SVG, `<script>` or `<style>`, or an element that holds text only): the block
177
+ * registers nothing (VAR-7) and renders what the catalog holds.
178
+ */
179
+ 'data-ls-unnamed'?: string;
110
180
  }
111
181
  /**
112
- * React wrapper around the vanilla `Translate` DOM class from
113
- * `langsys-js-typescript`. It renders a host element, then on mount lets the
114
- * vanilla class walk and tokenize the rendered children (text nodes plus
115
- * translatable attributes), register the content block, and re-translate on
116
- * locale change. On unmount it tears the instance down.
117
- *
118
- * This is the React analog of the Svelte `<Translate>` component — pure
119
- * mount/destroy glue. The DOM walking, content-block registration, attribute
120
- * harvesting, and re-translation lifecycle all live in the base SDK.
121
- *
122
- * Like the Svelte component, this lets the SDK mutate the rendered DOM in place,
123
- * so keep the children static: prose, marketing copy, CMS-rendered HTML — the
124
- * content-block use case. For dynamic per-string values that React owns and
125
- * re-renders, use `useT()` instead.
182
+ * Translates a block of static content: prose, markup, CMS copy.
183
+ *
184
+ * Content made of host elements, text and numbers renders through the core's `renderBlock`: the
185
+ * translated tree, the block's id and its markers come back as data, and this component renders
186
+ * them as React elements — the originals, cloned with their handlers, refs and keys, in the
187
+ * places the translation puts them. The same call runs on a server (inside a request scope) and
188
+ * in the browser (over the seeded catalog), so the first client render matches the server's HTML,
189
+ * and a locale change re-renders it. Registration goes through the core's `registerBlock`; no DOM
190
+ * class ever walks text nodes React owns.
191
+ *
192
+ * Content whose DOM is unknown until React renders it — a component, `lazy`, `Suspense`, or
193
+ * `dangerouslySetInnerHTML` — is rendered as it is, with an explicit `custom_id` stamped, and the
194
+ * core's DOM `Translate` class translates it after mount. On a server the core warns once per
195
+ * reason that such a block was served as source.
126
196
  */
127
- declare function Translate({ category, custom_id, label, params, tag, className, children, }: TranslateProps): react.ReactElement<{
197
+ declare function Translate({ category: named, previousCategory: namedPrevious, custom_id, label, params: given, tag, className, children: content, 'data-ls-unnamed': unmarked, }: TranslateProps): react.ReactElement<{
128
198
  ref: react.RefObject<HTMLElement | null>;
129
199
  className: string | undefined;
130
200
  }, string | react.JSXElementConstructor<any>>;
@@ -136,13 +206,34 @@ declare function Translate({ category, custom_id, label, params, tag, className,
136
206
  interface PhraseProps {
137
207
  /** Category the phrase registers under (disambiguation for translators). */
138
208
  category?: string;
139
- /** Interpolation params. Write placeholders as `%n%` / `%name%` in the markup (a bare `{n}` in JSX is an expression). */
209
+ /**
210
+ * The category this content was registered under before it moved to `category` (MOVE-1):
211
+ * `''` for none, absent for no move. Its translations follow it instead of being made, and
212
+ * charged for, again; until the catalog holds it under `category`, the previous category's
213
+ * entry renders and the host says so (MOVE-4, MOVE-5). Never part of its identity (MOVE-2),
214
+ * and sent only on the registration a miss triggers (MOVE-3), so once the catalog has it the
215
+ * prop does nothing and can be deleted whenever convenient.
216
+ */
217
+ previousCategory?: string | null;
218
+ /**
219
+ * Interpolation params. Write placeholders as `%n%` / `%name%` in the markup.
220
+ * A bare `{n}` in JSX is an expression React evaluates before the SDK sees the
221
+ * text, so the *value* lands in the captured phrase and each distinct value
222
+ * becomes its own phrase to translate. `%n%` yields one stable phrase.
223
+ */
140
224
  params?: Record<string, ParamPrimitive>;
141
225
  /** Host element tag. Defaults to `<span>`. */
142
226
  tag?: string;
143
227
  /** Class applied to the host element. */
144
228
  className?: string;
145
229
  children?: ReactNode;
230
+ /**
231
+ * Set by the build transform when every value of the phrase is marked (it holds a marker of its
232
+ * own) and one in its translated text stands where no marker can (in a translated `<svg>`, or an
233
+ * element that holds text only), so it is left as written: the phrase registers nothing (VAR-7)
234
+ * and renders what the catalog holds.
235
+ */
236
+ 'data-ls-unnamed'?: string;
146
237
  }
147
238
  /**
148
239
  * React wrapper around the vanilla `Phrase` rich-text handler.
@@ -158,6 +249,15 @@ interface PhraseProps {
158
249
  * expression React evaluates before the SDK sees the text. `%n%` passes through
159
250
  * as literal text and the SDK normalizes it to canonical `{n}` at capture.
160
251
  *
252
+ * The cost of getting this wrong is not a broken placeholder, it's catalog
253
+ * churn. `<Phrase>` keys on the encoded phrase string, so the substituted value
254
+ * becomes part of the key — a new phrase per distinct value, none reusable,
255
+ * with the base locale rendering correctly throughout. Measured against the
256
+ * shipped `encodeRichText`:
257
+ *
258
+ * {n} via JSX "Based on 0 {m0o}reviews{m0c}" (and 1, 2, … each new)
259
+ * %n% placeholder "Based on {n} {m0o}reviews{m0c}" (stable for all values)
260
+ *
161
261
  * The inline markup never reaches the translator — it's replaced with neutral
162
262
  * tokens and the real elements are reconstituted at render (see richtext.ts in
163
263
  * the base SDK). The host carries `data-ls-phrase` so a wrapping `<Translate>`
@@ -166,10 +266,9 @@ interface PhraseProps {
166
266
  * Keep children static (literal markup): the handler takes over the rendered
167
267
  * subtree. For values React owns and re-renders, pass them through `params`.
168
268
  */
169
- declare function Phrase({ category, params, tag, className, children }: PhraseProps): react.ReactElement<{
269
+ declare function Phrase({ category: named, previousCategory: namedPrevious, params: given, tag, className, children: content, 'data-ls-unnamed': unmarked, }: PhraseProps): react.ReactElement<{
170
270
  ref: react.RefObject<HTMLElement | null>;
171
271
  className: string | undefined;
172
- "data-ls-phrase": string;
173
272
  }, string | react.JSXElementConstructor<any>>;
174
273
 
175
274
  /**
@@ -192,7 +291,9 @@ interface DontTranslateProps {
192
291
  * The host carries the standard `translate="no"` attribute, which both the
193
292
  * tokenizer and the renderer in the base SDK already honor — so its content is
194
293
  * never tokenized, registered, or replaced. Pure presentational glue; no
195
- * vanilla handler needed.
294
+ * vanilla handler needed. Inside `<Translate>` / `<Phrase>` the block mapping reads it as the host
295
+ * element it renders (`toBlockNodes`), so the unit around it stays on the core's tree path, and the
296
+ * build transform marks a value inside it rather than replacing it (VAR-3).
196
297
  *
197
298
  * `data-ls-dont-translate` is a **wrapper-level hook only** — a styling and
198
299
  * debugging handle. The base SDK has never matched on it (verified against
@@ -207,46 +308,30 @@ declare function DontTranslate({ tag, className, children }: DontTranslateProps)
207
308
  className: string | undefined;
208
309
  }, string | react.JSXElementConstructor<any>>;
209
310
 
210
- /**
211
- * React-flavored init config. Identical to the base SDK's config except
212
- * `UserLocaleStore` is typed as a `Signal<string>` — create one with
213
- * `createLocaleStore()` (or get one from the `useLocaleStore` hook). The base
214
- * SDK only reads and subscribes to it.
215
- */
216
- interface iLangsysInitConfig extends Omit<iLangsysInitConfig$1, 'UserLocaleStore'> {
217
- UserLocaleStore: Signal<string>;
311
+ interface LangsysProviderProps {
312
+ /** The request's seed, from `scope.seed()` in a server component, passed down as a prop. */
313
+ seed: RequestSeed | null | undefined;
314
+ children?: ReactNode;
218
315
  }
219
316
  /**
220
- * React SDK entry point. Delegates everything to the underlying
221
- * `langsys-js-typescript` singleton. Because React's locale store is already a
222
- * `Signal` (unlike Svelte's `Writable`, which needs adapting), `init` is a
223
- * straight passthrough — the React-native concerns live in the hooks and the
224
- * `<Translate>` component, not here.
317
+ * Renders a request in its own locale and catalog where the framework renders client
318
+ * components separately from the code that opened the request's scope — Next.js's App Router,
319
+ * where server components and client components use separate copies of the SDK.
320
+ *
321
+ * // app/layout.tsx (a server component)
322
+ * const scope = await createRequestScope({ locale, url }); // from langsys-js-react/server
323
+ * return <LangsysProvider seed={scope.seed()}>{children}</LangsysProvider>;
324
+ *
325
+ * On the server, the provider rebuilds the request's scope from the seed with the core's
326
+ * `scopeFromSeed` and hands it down: `useT()`, `useCurrentLocale()`, `useTranslations()`,
327
+ * `<Translate>` and `<Phrase>` render from it. In the browser it seeds the SDK with the same
328
+ * catalog before any child's effect runs, and the hydration render reads the same scope, so the
329
+ * client's first render matches the server's HTML.
330
+ *
331
+ * Content a client component misses during the server render is registered by the browser after
332
+ * hydration. Under the `server` SSR strategy those misses are not sent from the server: the
333
+ * provider's side of the render has no after-response hook.
225
334
  */
226
- declare class LangsysAppReact {
227
- /** Initialize Langsys. Pass a `Signal<string>` (from `createLocaleStore`) as `UserLocaleStore`. */
228
- init(config: iLangsysInitConfig): Promise<iLangsysResponse>;
229
- get Translations(): langsys_js_typescript.Translations;
230
- get translationsLoadingPromise(): Promise<unknown>;
231
- /** Current translation function. Reads fresh state on every call (not reactive on its own — use `useT()` in components). */
232
- get t(): TFunction;
233
- get debug(): langsys_js_typescript.Logger;
234
- refresh(): Promise<boolean>;
235
- getCountries(inLocale?: string): Promise<iCountryList>;
236
- getCountryName(forCountryCode: string, inLocale?: string): Promise<string>;
237
- getCurrencies(inLocale?: string): Promise<iCurrencyList>;
238
- getCurrencyName(forCurrencyCode: string, inLocale?: string): Promise<string>;
239
- getDialCodes(inLocale?: string): Promise<iCountryDialCode[]>;
240
- getLocales(inLocale?: string): Promise<iLocaleDefault>;
241
- getLocalesFlat(inLocale?: string): Promise<iLocaleFlat[]>;
242
- getLocalesData(inLocale?: string, forceRefresh?: boolean): Promise<iLocaleData[]>;
243
- getLocalesFormat(format?: '' | 'flat' | 'data', inLocale?: string): Promise<iLocaleFlat[] | iLocaleDefault | iLocaleData[]>;
244
- getLocaleName(forLocale: string, shortName?: boolean, inLocale?: string): string;
245
- getLocaleNameWithLookup(forLocale: string, shortName?: boolean, inLocale?: string): Promise<string>;
246
- /** @deprecated use `getLocaleNameWithLookup` or `getLocaleName` */
247
- getLanguageName(forLocale: string, shortName?: boolean, inLocale?: string): Promise<string>;
248
- detectPreferredLocale(acceptLanguageHeader?: string | null, supportedLocales?: string[]): string | false;
249
- }
250
- declare const LangsysApp: LangsysAppReact;
335
+ declare function LangsysProvider({ seed, children }: LangsysProviderProps): react.FunctionComponentElement<react.ProviderProps<RequestScope | null>>;
251
336
 
252
- export { DontTranslate, type DontTranslateProps, LangsysApp, Phrase, type PhraseProps, Translate, type TranslateProps, createLocaleStore, type iLangsysInitConfig, useCurrentLocale, useLocaleStore, useSignal, useT, useTranslations };
337
+ export { DontTranslate, type DontTranslateProps, LangsysProvider, type LangsysProviderProps, Phrase, type PhraseProps, Translate, type TranslateProps, useCurrentLocale, useLocaleStore, useNotifyNavigation, useRenderServerMessage, useSignal, useT, useTranslations, useWriteEnabled };