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/CHANGELOG.md +43 -0
- package/README-SSR.md +144 -170
- package/README.md +339 -23
- package/dist/app-zQKL7-vo.d.mts +61 -0
- package/dist/app-zQKL7-vo.d.ts +61 -0
- package/dist/babel.d.mts +122 -0
- package/dist/babel.d.ts +122 -0
- package/dist/babel.js +547 -0
- package/dist/babel.js.map +1 -0
- package/dist/babel.mjs +545 -0
- package/dist/babel.mjs.map +1 -0
- package/dist/index.d.mts +164 -79
- package/dist/index.d.ts +164 -79
- package/dist/index.js +502 -97
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +423 -89
- package/dist/index.mjs.map +1 -1
- package/dist/next-loader.d.mts +8 -0
- package/dist/next-loader.d.ts +8 -0
- package/dist/next-loader.js +623 -0
- package/dist/next-loader.js.map +1 -0
- package/dist/next-loader.mjs +621 -0
- package/dist/next-loader.mjs.map +1 -0
- package/dist/next.d.mts +15 -0
- package/dist/next.d.ts +15 -0
- package/dist/next.js +41 -0
- package/dist/next.js.map +1 -0
- package/dist/next.mjs +37 -0
- package/dist/next.mjs.map +1 -0
- package/dist/server.d.mts +36 -0
- package/dist/server.d.ts +36 -0
- package/dist/server.js +57 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +25 -0
- package/dist/server.mjs.map +1 -0
- package/dist/vite.d.mts +14 -0
- package/dist/vite.d.ts +14 -0
- package/dist/vite.js +623 -0
- package/dist/vite.js.map +1 -0
- package/dist/vite.mjs +621 -0
- package/dist/vite.mjs.map +1 -0
- package/package.json +38 -3
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ If you need the SDK outside React (a Node script, a non-React web app), import f
|
|
|
33
33
|
npm install langsys-js-react
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
`langsys-js-typescript` is installed automatically as a
|
|
36
|
+
`langsys-js-typescript` (`^0.7.3`) is installed automatically as a dependency. `react` is a peer dependency you already have.
|
|
37
37
|
|
|
38
38
|
## Creating a Langsys project
|
|
39
39
|
|
|
@@ -67,7 +67,6 @@ export function LangsysGate({ children }: { children: ReactNode }) {
|
|
|
67
67
|
UserLocaleStore: localeStore,
|
|
68
68
|
baseLocale: 'en-US',
|
|
69
69
|
debug: false,
|
|
70
|
-
ssrTokenStrategy: 'client',
|
|
71
70
|
}).then((res) => {
|
|
72
71
|
if (res.status) setReady(true);
|
|
73
72
|
else setError(res.errors?.join(', ') ?? 'Init failed');
|
|
@@ -82,15 +81,29 @@ export function LangsysGate({ children }: { children: ReactNode }) {
|
|
|
82
81
|
|
|
83
82
|
`UserLocaleStore` is a `Signal<string>` — switch it with `setLocale(...)` (from the same `useLocaleStore` call) or `localeStore.set('fr-FR')`, and the SDK reacts. If you'd rather keep the locale store at module scope, `const localeStore = createLocaleStore('en-US')` works too.
|
|
84
83
|
|
|
85
|
-
Locale identifiers are canonicalized
|
|
84
|
+
Locale identifiers are canonicalized by the base SDK: `'en-US'`, `'EN-US'` and `'en-us'` all normalize to **lowercase** `'en-us'`. `useCurrentLocale()` and `detectPreferredLocale()` return that lowercase form, so compare against `'en-us'` — a test for `'en-US'` never matches. Normalize your own values with the re-exported `canonicalizeLocale()` rather than casing by hand.
|
|
85
|
+
|
|
86
|
+
### Running without credentials
|
|
87
|
+
|
|
88
|
+
`init()` without a project id, an API key or a locale store does not throw: it resolves `{ status: false, errors }` and leaves the client unconfigured, so a preview build or a test run without secrets still renders. `t()` shows the source text with its params filled, `<Translate>` and `<Phrase>` show their content as written, and nothing is requested or registered: a registration resolves skipped with the `RegistrationReason` `'not-configured'`. An explicit write, such as `LangsysAppAPI.createTranslatableItems`, throws `LangsysConfigurationError`. `LangsysApp.isConfigured()` reports the state.
|
|
89
|
+
|
|
90
|
+
To fail loudly instead, pass `requireCredentials: true`. While the client is unconfigured:
|
|
91
|
+
|
|
92
|
+
- `t()` throws `LangsysConfigurationError`, so a component that calls it throws during render, and the error reaches your nearest error boundary.
|
|
93
|
+
- `<Translate>` and `<Phrase>` still show their content as written. Their registration rejects with the same error, including a block that mounted before `init()` answered, left unhandled so it appears as an unhandled rejection in the console, and fails a test runner that treats those as errors. A block holding a value the build transform did not name registers nothing, so it only reads and stays silent.
|
|
94
|
+
- On a server, the request's `t()` throws and a block's registration never rejects.
|
|
95
|
+
|
|
96
|
+
`LangsysConfigurationError` is exported from `langsys-js-react` and from `langsys-js-react/server`.
|
|
86
97
|
|
|
87
98
|
### SSR token strategy
|
|
88
99
|
|
|
89
|
-
`ssrTokenStrategy` (default `'client'`)
|
|
100
|
+
`ssrTokenStrategy` (default `'client'`) decides what a request scope's `close()` does with the phrases and blocks its [server render](#server-side-rendering-nextjs-remix) missed. The scope reads it from `LangsysApp.init()` in the rendering process, so it applies only where the server calls `init()` too. A client-only app like the one above has no server render, and a server that never calls `init()` sends nothing from the server.
|
|
90
101
|
|
|
91
|
-
- `'client'` —
|
|
92
|
-
- `'server'` —
|
|
93
|
-
- `'auto'` —
|
|
102
|
+
- `'client'` (default) — the server sends nothing. The browser registers the misses after hydration when its session may write, and otherwise reports the page for discovery.
|
|
103
|
+
- `'server'` — `close()` sends the misses from the server, from your server's IP rather than a browser's.
|
|
104
|
+
- `'auto'` — `close()` sends a list of fewer than five from the server and leaves a longer one to the browser.
|
|
105
|
+
|
|
106
|
+
The server sends only when its own key may write; otherwise its misses go to the browser as under `'client'`. With a `writeGrant` configured the server sends nothing under any strategy, because a grant makes write capability per user, and the SDK warns that the strategy is degraded to `'client'`. [README-SSR.md](./README-SSR.md#what-a-server-render-translates-and-discovers) lists what a server render translates and what it discovers.
|
|
94
107
|
|
|
95
108
|
## Using translations
|
|
96
109
|
|
|
@@ -141,9 +154,39 @@ t('You have {count} new messages', 'Notifications', { count: 3, extra: 'x' });
|
|
|
141
154
|
// ❌ Object literal may only specify known properties, and 'extra' does not exist
|
|
142
155
|
```
|
|
143
156
|
|
|
144
|
-
Allowed value types: `string | number | Date | boolean`.
|
|
157
|
+
Allowed value types: `string | number | Date | boolean`. Values are locale-formatted: numbers go through `Intl.NumberFormat` (`1234.5` → `1.234,5` in `de-DE`) and `Date` values through `Intl.DateTimeFormat` with the medium date style. Pass a string to opt out of formatting. Formatting always uses the catalog locale (falling back to `en`), never the host's default locale, so server and client render identically. The same holds inside a translation's plural or gender form.
|
|
158
|
+
|
|
159
|
+
#### Plurals and gender: write the sentence flat
|
|
160
|
+
|
|
161
|
+
**Never write ICU.** Write the sentence as it reads, with the count or the name as a `{placeholder}`, and pass the value as a param. Langsys makes each language's plural and gender forms, the base language included:
|
|
145
162
|
|
|
146
|
-
|
|
163
|
+
```tsx
|
|
164
|
+
t('You have {count} new messages', 'Inbox', { count }); // ✅
|
|
165
|
+
t('{count, plural, one {# new message} other {# new messages}}', 'Inbox', { count }); // ❌ ICU written by hand
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Langsys makes the forms once it has checked the phrase: a translation run checks its own phrases, and an hourly check covers any phrase a run missed. Until then the base language shows the sentence as written. The catalog holds the forms as ICU MessageFormat. Every request carries `X-Langsys-Capabilities: icu`, and `t()` renders the form the count selects with the CLDR plural rules of the loaded catalog's locale (Russian's four categories, Arabic's six). A plural or select renders its `other` branch when its argument isn't passed. Your code passes the same params in every language. A count and the noun it inflects belong in one phrase, or no plural form can reach the noun.
|
|
169
|
+
|
|
170
|
+
A sentence that says something different at zero is its own phrase, chosen in code, and so is a sentence that differs in the source language itself:
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
count === 0 ? t('Your inbox is empty.', 'Inbox') : t('You have {count} new messages', 'Inbox', { count });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
In a language with grammatical gender, Langsys can turn a plain `{name}` into `{name_gender, select, …}` in the translation. Pass `name_gender` (`'female'`, `'male'` or `'other'`) beside `name` when you know it. `t()` types it as an optional param for every placeholder, `<Phrase params>` and `<Translate params>` take it as it is, and without it the neutral `other` form renders:
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
t('{name} has been invited', 'Team', { name: 'Ana', name_gender: 'female' });
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
A plain `{n}` formats a number or a date, and nothing else needs writing. That holds inside a translation that carries a plural or gender form too: the count, written `#` or `{count}`, and every other number format for the loaded catalog's locale. A value passed as a string is written as given, so pass a time of day, or a number in a format of your own, as a string formatted with the loaded locale:
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
const locale = useCurrentLocale() || 'en'; // '' until the first catalog loads, and Intl throws on ''
|
|
186
|
+
const total = new Intl.NumberFormat(locale, { style: 'currency', currency: 'EUR' }).format(amount);
|
|
187
|
+
const at = new Intl.DateTimeFormat(locale, { timeStyle: 'short' }).format(date);
|
|
188
|
+
t('{count} items, {total}, ready at {at}', 'Orders', { count, total, at });
|
|
189
|
+
```
|
|
147
190
|
|
|
148
191
|
#### Categorization disambiguates context
|
|
149
192
|
|
|
@@ -158,6 +201,41 @@ Without categorization, "Home" would only have one translation — which can't w
|
|
|
158
201
|
|
|
159
202
|
A good rule for category names: the module or feature the phrase lives in (`Account`, `Errors`, `Checkout`, `UI`).
|
|
160
203
|
|
|
204
|
+
#### Moving content to another category
|
|
205
|
+
|
|
206
|
+
The category is part of what identifies a phrase or block, so changing it registers the content as new, and it would be translated, and charged for, again. Name the category it was in before, and its translations follow it instead:
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
import { inCategory, Phrase, Translate } from 'langsys-js-react';
|
|
210
|
+
|
|
211
|
+
t('Buy now', inCategory('Checkout', { previousCategory: 'Header' }));
|
|
212
|
+
|
|
213
|
+
<Phrase category="Checkout" previousCategory="Header">Buy <b>now</b></Phrase>
|
|
214
|
+
|
|
215
|
+
<Translate category="Checkout" previousCategory="Header">
|
|
216
|
+
<p>Free shipping on every order.</p>
|
|
217
|
+
</Translate>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`t()`'s second argument is a category when it's a string and params when it's an object, so a move goes in the category position, wrapped by `inCategory()`. Use `previousCategory: ''` if the content had no category before.
|
|
221
|
+
|
|
222
|
+
- **It moves every use.** Langsys keeps one item per text and category, so a move retires the old item for every call that registers that text in that category. Move them all in one release.
|
|
223
|
+
- **Nothing to clean up on a schedule.** The previous category is sent once, on the registration the new code triggers. Once the catalog has the content under its new category, the option does nothing and you can delete it whenever convenient.
|
|
224
|
+
- **No flash on the first deploy.** Until the catalog has the new category, the previous category's translation renders, and the host says where the text came from, `data-ls-previous-contentblock` when it was filled from the old category's block and `data-ls-previous-category` when it was filled from the old category's phrase, so hydration renders the same text.
|
|
225
|
+
- **A block with its own `custom_id` moves in place.** Its id names it whatever its category, so the API moves the block itself, and an older release still registering it under the old category moves it back. Move every use in one release.
|
|
226
|
+
- **On a server entry** (`langsys-js-react/server`), import `inCategory` from there: it is the same function the main entry exports, and `langsys-js-typescript` is a dependency of this package, so importing it directly breaks under pnpm and Yarn PnP.
|
|
227
|
+
- **A nested unit that names no category moves with its block.** A `<Phrase>` or `<Translate>` inside a `<Translate>` (or `<Phrase>`) that holds a component takes the block's `category` and its `previousCategory`, since it is part of the same content. Leave `category` off to inherit; `category=""` names no category and takes neither. A nested unit that names its own `category` takes neither, and moves only if it names its own `previousCategory`. One that names no category but a `previousCategory` of its own takes the block's category and its own previous one. A unit that is not inside a block names what its props say. Markup does the same: a host nested in a block takes the block's category and previous category unless it carries its own `data-ls-category`, and `data-ls-previous-category` beside it names its own previous category, the way the core stamps a host rendered from the previous category.
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
<Translate category="Checkout" previousCategory="Header">
|
|
231
|
+
<p>Welcome, <UserName /></p>
|
|
232
|
+
<Phrase>Free shipping</Phrase> {/* Checkout, moved from Header */}
|
|
233
|
+
<Phrase category="Cart">Your cart</Phrase> {/* Cart, no move */}
|
|
234
|
+
<Phrase category="Cart" previousCategory="Basket">Your cart</Phrase> {/* Cart, moved from Basket */}
|
|
235
|
+
</Translate>
|
|
236
|
+
```
|
|
237
|
+
- **Needs base SDK `langsys-js-typescript@^0.7.3`**, which carries the move.
|
|
238
|
+
|
|
161
239
|
### `<Translate>` — HTML content blocks
|
|
162
240
|
|
|
163
241
|
For larger blocks of HTML where the structure should be preserved for the translator:
|
|
@@ -182,11 +260,13 @@ The component:
|
|
|
182
260
|
- **Screen-reader text:** `aria-label`, `aria-placeholder`, `aria-description`, `aria-valuetext`, `aria-roledescription` — worth knowing these are covered, since untranslated ARIA strings are invisible on the page and only surface to someone using a screen reader
|
|
183
261
|
- **Validation messages:** `data-error`, `data-error-message`, `data-validation-message`, `data-invalid-message`, `data-required-message`, `data-pattern-message`
|
|
184
262
|
- Translates the `value` attribute **only where it is a label rather than data**: on `<button>`, and on `<input type="submit">` / `<input type="button">`. Every other input type is left alone, so a text field's value is never rewritten. This is a separate mechanism from the attribute list above — `value` is deliberately *not* in the SDK's `TRANSLATABLE_ATTRIBUTES`; it's gated by `VALUE_TRANSLATABLE_ELEMENTS` / `VALUE_TRANSLATABLE_INPUT_TYPES`.
|
|
185
|
-
-
|
|
263
|
+
- Registers a snapshot of the block's markup, so translators see its structure in the Translation Manager.
|
|
186
264
|
- Registers the whole thing as a **content block** that translators handle as one unit while still translating the individual phrases inside.
|
|
187
265
|
- Auto re-translates on locale change.
|
|
188
266
|
|
|
189
|
-
`<Translate>`
|
|
267
|
+
`<Translate>` renders its content through the core: the translated text comes back as data, and the component renders it as React elements — your own elements, with their handlers, refs and keys, in the places the translation puts them. It renders the same on a server and in the browser, so a server-rendered page arrives translated and hydrates without a mismatch, and a locale change re-renders it. Keep its children to markup and text — prose, marketing copy, forms with placeholders. For dynamic per-string values that React owns, use `useT()`.
|
|
268
|
+
|
|
269
|
+
A block whose children hold a component, `lazy`, `Suspense` or `dangerouslySetInnerHTML` cannot be rendered that way, because its content isn't known until React renders it. Such a block renders its children as they are, and the SDK translates it in the browser after it mounts; its `custom_id`, when you give one, is on the element from the first render. The SDK registers such a block once its content has stopped changing for 500 ms, so a `Suspense` fallback that resolves within that time is never registered as the block. A fallback still showing then is registered, because it is what the page shows; if it resolves later, the SDK says so in a debug notice naming both block ids. On a server the SDK warns once that it served such a block in the source language. Until it mounts, such a block shows its children as the build transform left them, `%name%` placeholders included, so keep components out of a block that interpolates values when its server HTML matters: put the component outside the `<Translate>`, or the values in a `<Phrase>` of their own.
|
|
190
270
|
|
|
191
271
|
```tsx
|
|
192
272
|
{/* CMS content goes through Translate as-is */}
|
|
@@ -195,7 +275,15 @@ The component:
|
|
|
195
275
|
</Translate>
|
|
196
276
|
```
|
|
197
277
|
|
|
198
|
-
`<Translate>`
|
|
278
|
+
Values in `<Translate>` and `<Phrase>` markup are placeholders. With the build transform enabled (one line of configuration, see [Variables in translated text](#variables-in-translated-text)), write them as you would any JSX:
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
<Translate category="Cart">You have {items.length} items in your cart.</Translate>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
The transform turns `{items.length}` into the placeholder `{items_count}` and passes the value as its param, so the sentence registers once — `You have {items_count} items in your cart.` — for every user and every count, and a translator can give it plural forms.
|
|
285
|
+
|
|
286
|
+
Without the transform, write the placeholder yourself as **`%key%`** and pass the value in `params`:
|
|
199
287
|
|
|
200
288
|
```tsx
|
|
201
289
|
<Translate category="Cart" params={{ count: itemCount }}>
|
|
@@ -203,11 +291,11 @@ The component:
|
|
|
203
291
|
</Translate>
|
|
204
292
|
```
|
|
205
293
|
|
|
206
|
-
|
|
294
|
+
`%key%` passes through JSX as plain text and the SDK reads it as `{key}`; keys are identifier-shaped (`%[A-Za-z_][A-Za-z0-9_]*%`), so a stray `%` in prose ("50% off") is left alone. `params` apply to text, translatable attributes, `<option>` text and single-token content; numbers and dates get the locale's formatting, inside a plural or gender form too, and changing `params` re-renders the block.
|
|
207
295
|
|
|
208
|
-
|
|
296
|
+
Without the transform, a bare `{count}` is evaluated by React before the SDK sees the text. Where it sits inside other text — `You have {count} items` — the component can tell a value was there but not what to call it, so it registers nothing for that block, renders any translation the catalog already holds, and says so once as a debug notice. In a `<Phrase>`, a value inside a part that is not translated, or inside notation such as `<math>`, doesn't count: the phrase's key leaves that part out, so the phrase still registers. Where the value is the only text of its element (`<b>{name}</b>`), it cannot be told from literal text, and the block registers the text it renders. The transform leaves a conditional (`{signedIn ? name : 'guest'}`) as written, so one inside other text registers nothing either. Raw HTML is content, not a variable: a block whose only dynamic part is `dangerouslySetInnerHTML` (a CMS field, say) registers that HTML's text; one that also interpolates a value registers nothing.
|
|
209
297
|
|
|
210
|
-
`<Translate>` props: `category?`, `custom_id?`, `label?`, `params?`, `tag?` (defaults to `translate`), `className?`, `children`.
|
|
298
|
+
`<Translate>` props: `category?`, `previousCategory?` (see [Moving content to another category](#moving-content-to-another-category)), `custom_id?`, `label?`, `params?`, `tag?` (defaults to `translate`), `className?`, `children`.
|
|
211
299
|
|
|
212
300
|
### `<Phrase>` — markup-bearing phrases (pluralization)
|
|
213
301
|
|
|
@@ -225,7 +313,7 @@ The inline elements never reach the translator — they're replaced with neutral
|
|
|
225
313
|
|
|
226
314
|
> Note: write placeholders as `%n%` in `<Phrase>` markup (see the `<Translate>` note above) — a bare `{n}` in JSX is an expression and never reaches the SDK.
|
|
227
315
|
|
|
228
|
-
`<Phrase>` props: `category?`, `params?`, `tag?` (defaults to `span`), `className?`, `children`.
|
|
316
|
+
`<Phrase>` props: `category?`, `previousCategory?`, `params?`, `tag?` (defaults to `span`), `className?`, `children`.
|
|
229
317
|
|
|
230
318
|
### `<DontTranslate>` — never-translated regions
|
|
231
319
|
|
|
@@ -237,10 +325,59 @@ import { DontTranslate } from 'langsys-js-react';
|
|
|
237
325
|
Built with <DontTranslate>Kangen®</DontTranslate> on <DontTranslate>langsys.dev</DontTranslate>
|
|
238
326
|
```
|
|
239
327
|
|
|
240
|
-
Renders the host with `translate="no"`, which the base SDK's tokenizer and renderer already honor — the content is never tokenized, registered, or replaced.
|
|
328
|
+
Renders the host with `translate="no"`, which the base SDK's tokenizer and renderer already honor — the content is never tokenized, registered, or replaced. Inside `<Translate>` and `<Phrase>` it is that host element, so the block around it renders on the server like any other markup. A value written inside it is shown as it is; see [Variables in translated text](#variables-in-translated-text) for how the build transform keeps it out of what Langsys stores.
|
|
241
329
|
|
|
242
330
|
`<DontTranslate>` props: `tag?` (defaults to `span`), `className?`, `children`.
|
|
243
331
|
|
|
332
|
+
## Variables in translated text
|
|
333
|
+
|
|
334
|
+
A sentence that shows a value — a name, a count, a date — is one phrase however many values it is shown with. The build transform makes that automatic: inside `<Translate>`, `<Phrase>` and `t()` calls it turns each interpolated value into a named placeholder and passes the value as its param.
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
<Translate>Hello {user.firstName}, welcome back</Translate>
|
|
338
|
+
// registers "Hello {first_name}, welcome back"
|
|
339
|
+
|
|
340
|
+
<Phrase>Signed in as <b>{user.name}</b></Phrase>
|
|
341
|
+
// registers "Signed in as {m0o}{name}{m0c}"
|
|
342
|
+
|
|
343
|
+
t(`You have ${inbox.count} new messages`)
|
|
344
|
+
// registers "You have {inbox_count} new messages"
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Enable it with one line, for your build tool:
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
// vite.config.ts
|
|
351
|
+
import langsys from 'langsys-js-react/vite';
|
|
352
|
+
export default defineConfig({ plugins: [langsys(), react()] });
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
```js
|
|
356
|
+
// next.config.mjs — Turbopack and webpack; Next's own compiler stays on
|
|
357
|
+
import { withLangsys } from 'langsys-js-react/next';
|
|
358
|
+
export default withLangsys({ /* your config */ });
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
```json
|
|
362
|
+
// babel.config.json — any other Babel setup
|
|
363
|
+
{ "plugins": ["langsys-js-react/babel"] }
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
It only touches files that import `langsys-js-react`, and reads them with TypeScript's or the standard decorators and, in a `.js` file, Flow; a file it still can't read is left as written, with a build warning, for your compiler to report, and only `<Translate>` and `<Phrase>` imported from it and the `t` a `useT()` call returns; another function named `t` in the same file is left alone. In a `t()` call, the values join the params where `t()` reads them, whichever form you wrote (`t(text, params)` or `t(text, category, params)`); a second argument the build can't see into, such as a variable, is read when the call runs, as `t()` reads it. `+` joins as JavaScript joins it (`'Page ' + (page + 1)` is one value), a name you wrote (`%name%` anywhere in a block, a key in a params object you wrote out, or `{name}` in a `t()` text whose params come from a variable or a spread) is never given to another value, and an argument whose own text has no space (`t(`errors.${code}`)`, `t(`status_${status}`)`) is a key built in the call and is left as it is, so legacy-key mode looks it up. String, number and plain template literals stay text, a fragment's content joins the text around it, and each run of text reaches React as one string, with JSX's whitespace rules applied as written, whether the plugin runs in your Babel config or ahead of another compiler. JSX, conditionals and components inside a block are left as they are, and so is any expression holding JSX (`{items.map((i) => <li key={i}>{i}</li>)}`), which renders elements rather than a value's text. `{children}` that is all the text between two elements is content: a wrapper like `<Translate tag="h1">{children}</Translate>` translates what it is given. Beside text, as in `Read {children}`, or inside a part that is not translated, it is a value like any other. When the page renders, a value that is text, a number, a boolean or a date is the placeholder's param; a boolean stays a boolean, so a select in the translation can branch on it, as it can in `t()`. A bigint is passed as its digits, and `null` or `undefined` renders nothing, as React would render it. On React 19 a promise or a context is read as React reads one, a context left in the content too. Anything else, such as an element, a list or a function, is rendered in its place exactly as React renders it (`Click {icon} to go`).
|
|
367
|
+
|
|
368
|
+
In a `<Translate>`, the attributes the SDK translates (`alt`, `title`, `placeholder`, `aria-label`, `label` and the rest of its list, and the `value` of a button or a submit button) are part of the block, so a value in one is a placeholder too: `<img alt={`Photo of ${user.name}`} />` registers `Photo of {name}`. A value that appears only inside attributes' text is written as JavaScript writes it into a string. A value that is the whole attribute (`alt={product.name}`), or that also appears in the text, fills its placeholder as a value in the text does: `null` or `undefined` fills it with nothing (`alt=""`), a number or a date is formatted for the locale, and a list or an object is written as React writes an attribute, or, inside an attribute's text, as JavaScript writes it into a string. On an element whose `translate`, `data-notrans` or input `type` is an expression, only the page knows whether the attribute is translated, so the value is left as written and that block registers nothing. A `<Phrase>` doesn't translate its elements' attributes, so they are left as written there. A value right after a `%` and a word (`%s{x}`) is written as `{x}`, the form the SDK stores, since `%s%x%` would read as the placeholder `%s%`.
|
|
369
|
+
|
|
370
|
+
A value inside a part that is not translated — `<DontTranslate>`, an element with `translate="no"` or `data-notrans`, or code such as `<template>` and `<noscript>` — is shown exactly as written, so the transform leaves it where it is and marks it as a value (`<span data-ls-param="email">`) instead of replacing it. A `translate` or `data-notrans` whose value is an expression is only known when the page renders. In a `<Translate>` a value under it is marked too; a marked value reads as its placeholder wherever it ends up. In a `<Phrase>` it is a placeholder like any other, filled whether the element turns out translated or not. A `<Phrase>` holding a marker you wrote yourself (`<span data-ls-param="count">{count}</span>`) has its other values marked too, so it reads correctly before a translation exists; a value in its translated text that stands where no marker can (in a translated `<svg>`, `<option>`, `<title>` or `<textarea>`) is left as written, and that phrase registers nothing. A name a marker of yours carries is never given to another value. Langsys never stores a marked value: a block's stored content holds the placeholder `{email}` in its place, and a `<Phrase>` keys the whole part as one empty piece of markup (`Signed in as {m0o}{m0c} today`), whatever it holds. Under such an expression, a value that turns out to be translated is filled from its param, formatted like any other. Some parts can't take the marker: inside `<math>`, and inside an `<svg>` that is not translated, a `<span>` draws nothing, and `<textarea>`, `<title>` and `<option>` hold text only. A value in such a part is left as written. A `<Translate>` holding one registers nothing, since its stored content would hold the value. A `<Phrase>` keys that part as one empty piece of markup and registers as usual. In translated text inside an `<svg>`, a value is a placeholder like anywhere else. `<script>`, `<style>` and `<plaintext>` hold raw text, so a value there is left as written, and a `<Translate>` holding one registers nothing. An opt-out set through a spread (`<span {...props}>`) is invisible to the build: write `translate` or `data-notrans` on the element itself.
|
|
371
|
+
|
|
372
|
+
```tsx
|
|
373
|
+
<Translate>
|
|
374
|
+
<p>Signed in as <DontTranslate>{user.email}</DontTranslate></p>
|
|
375
|
+
</Translate>
|
|
376
|
+
// registers the tokens "Signed in as"; the page shows the address
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
**Names** are the same in every Langsys SDK, derived from the expression in snake_case: `user.firstName` → `first_name`, `items.length` → `items_count`, `price.value` → `price`, `formatDate(order.date)` → `date`. Two values that would share a name are told apart by the segment before it (`a.name`, `b.name` → `a_name`, `b_name`), and the same named expression written twice is one placeholder. An expression no name can be derived from — `a + b`, a ternary, a call with several arguments — is named `value`, `value_2`, …, one placeholder each time it appears, even when the same expression repeats, and the build warns about it; give it a name yourself with `%name%` and `params`, which always wins.
|
|
380
|
+
|
|
244
381
|
## Hooks & reactive primitives
|
|
245
382
|
|
|
246
383
|
| Export | Type | Notes |
|
|
@@ -252,13 +389,166 @@ Renders the host with `translate="no"`, which the base SDK's tokenizer and rende
|
|
|
252
389
|
| `useSignal(signal)` | `<T>(s: Signal<T>) => T` | Low-level: subscribe a component to any base-SDK signal. |
|
|
253
390
|
| `createLocaleStore(initial?)` | `(s?: string) => Signal<string>` | Make a user-locale store outside React (module scope). |
|
|
254
391
|
| `t` / `currentlyLoadedLocale` / `sTranslations` | `Signal<…>` | Raw signals for direct subscription outside React. In components, prefer the hooks. |
|
|
255
|
-
| `canonicalizeLocale(locale)` | `(s: string) => string` | Normalize a locale identifier to
|
|
392
|
+
| `canonicalizeLocale(locale)` | `(s: string) => string` | Normalize a locale identifier to the lowercase wire form (`'en-US'` → `'en-us'`) — the same normalization the SDK applies internally. |
|
|
393
|
+
| `useNotifyNavigation(location)` | `(location: unknown) => void` | Tells the SDK the route changed whenever `location` changes. See [Route changes](#route-changes). |
|
|
394
|
+
| `useRenderServerMessage()` | `() => (entry, category?) => string` | Renders server message entries, re-rendering when the locale changes. See [Server messages](#server-messages). |
|
|
395
|
+
| `useWriteEnabled()` | `() => boolean \| undefined` | Whether this session may register content, as decided by the server. See [Write gating](#write-gating) — the `undefined` state is meaningful. |
|
|
396
|
+
|
|
397
|
+
## Route changes
|
|
398
|
+
|
|
399
|
+
Call `useNotifyNavigation` once, anywhere under your router, with the router's current location:
|
|
400
|
+
|
|
401
|
+
```tsx
|
|
402
|
+
import { useNotifyNavigation } from 'langsys-js-react';
|
|
403
|
+
|
|
404
|
+
// React Router
|
|
405
|
+
function NavigationNotifier() {
|
|
406
|
+
useNotifyNavigation(useLocation().key);
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Next.js App Router
|
|
411
|
+
function NavigationNotifier() {
|
|
412
|
+
useNotifyNavigation(usePathname() + '?' + useSearchParams());
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Content that stays mounted across a route change — a header, a sidebar, a persistent layout — is not re-rendered by React when only the route changes, so without this the SDK never learns that its phrases now appear on the new page, and discovery never credits that page with them. The hook tells the SDK the route changed; every mounted translated node then looks itself up again at the new URL. Everything else — what is reported, when, and how often — is decided by the SDK.
|
|
418
|
+
|
|
419
|
+
If your router exposes its own after-navigation callback, calling `notifyNavigation()` there is equivalent.
|
|
420
|
+
|
|
421
|
+
## Server messages
|
|
422
|
+
|
|
423
|
+
A Langsys server SDK leaves your framework's own error response exactly as it is and attaches a list of entries beside it — for Laravel, under `langsys_errors` next to the usual `errors` map in the 422 body. Each entry carries `template`, the source sentence with `{markers}`; `params`, the values that fill them; and `message`, the same sentence already filled. It also passes through what the framework reports about the failure, unchanged: `field`, in the framework's own path format, and `code`, the framework's own identifier for the failure (Laravel's `Min`, Pydantic's `string_too_short`) — use it for your logic, never to choose text.
|
|
424
|
+
|
|
425
|
+
`resolveServerMessages(body, { key })` reads the entries from where the server attached them; `useRenderServerMessage()` renders them in the current locale:
|
|
426
|
+
|
|
427
|
+
```tsx
|
|
428
|
+
import { resolveServerMessages, useRenderServerMessage } from 'langsys-js-react';
|
|
429
|
+
|
|
430
|
+
function FormErrors({ body }: { body: unknown }) {
|
|
431
|
+
const render = useRenderServerMessage();
|
|
432
|
+
return (
|
|
433
|
+
<ul>
|
|
434
|
+
{resolveServerMessages(body, { key: 'langsys_errors' }).map((entry, i) => (
|
|
435
|
+
<li key={i}>{render(entry)}</li>
|
|
436
|
+
))}
|
|
437
|
+
</ul>
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Pass the same `key` the server SDK is configured with — a dotted path such as `meta.langsys` works too — or a `resolver` that maps the body to entries yourself. One of the two is required: the body is never searched by shape. If the server renames the pieces, pass `pieces`, e.g. `{ template: 'sentence', message: 'text' }`. An entry needs a `template` or a `message`.
|
|
443
|
+
|
|
444
|
+
An entry renders as the translation of its `template`, filled from `params`, when the catalog has one, and as its `message` otherwise — so a message nobody has translated yet still reads correctly. An entry with no `template` shows its `message`, and `message` is never used as a lookup key. Templates are looked up under one category, `Errors` unless `messagesCategory` is set in `init`, and it has to match the category the server registers them under. An app that renames that category sets `messagesPreviousCategory` to the old name, on the server and in `init` alike: a template with no translation under the new category then renders the old category's translation, and its `message` only when neither has one. The rendering function changes identity whenever the catalog or locale does, so it can be passed to memoised children.
|
|
445
|
+
|
|
446
|
+
### Inertia
|
|
447
|
+
|
|
448
|
+
After a failed form, the server SDK flashes the entries and shares them with the page it redirects to as a prop under the same key (`langsys_errors` in the Laravel package, unless configured otherwise). Resolve them from the page's props:
|
|
449
|
+
|
|
450
|
+
```tsx
|
|
451
|
+
import { usePage } from '@inertiajs/react';
|
|
452
|
+
|
|
453
|
+
function CardsNew() {
|
|
454
|
+
const render = useRenderServerMessage();
|
|
455
|
+
return resolveServerMessages(usePage().props, { key: 'langsys_errors' }).map((entry, i) => (
|
|
456
|
+
<p key={i}>{render(entry)}</p>
|
|
457
|
+
));
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
A page that follows no failure has no such prop, and renders nothing.
|
|
462
|
+
|
|
463
|
+
## Migrating from key-based i18n
|
|
464
|
+
|
|
465
|
+
An app moving from i18next or vue-i18n can keep calling `t()` with its existing keys. Give `init` the source-language files it already has:
|
|
466
|
+
|
|
467
|
+
```tsx
|
|
468
|
+
import en from './locales/en.json';
|
|
469
|
+
|
|
470
|
+
LangsysApp.init({
|
|
471
|
+
projectid,
|
|
472
|
+
key,
|
|
473
|
+
UserLocaleStore: store,
|
|
474
|
+
legacyKeys: [{ name: 'en.json', format: 'i18next', data: en }],
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
const t = useT();
|
|
478
|
+
t('checkout.title', undefined, { name: 'Ada' }); // "Checkout for Ada" — registered as "Checkout for {name}" under "checkout"
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
`t()` then resolves its argument as a key first. A key the files hold becomes its value, converted to Langsys placeholders and ICU plurals, and that value — never the key — is the phrase Langsys registers and translates; the key's first segment is its category unless the call passes one. An argument the files do not hold is literal source text. Formats are `i18next`, `vue-i18n` and `plain` (the default); any other makes `init` throw a `LegacyFormatError` naming the file. Leave `legacyKeys` unset and `t()` does no key lookup at all. `LangsysApp.Translations.setLegacyKeys(files | null)` turns the mode on or off later.
|
|
482
|
+
|
|
483
|
+
## Catalog snapshots
|
|
484
|
+
|
|
485
|
+
A snapshot is a project's catalog exported to a file, so a first paint or an offline session renders translations with no API call. Load it before the first render:
|
|
486
|
+
|
|
487
|
+
```tsx
|
|
488
|
+
import snapshot from './langsys-snapshot.json';
|
|
489
|
+
|
|
490
|
+
LangsysApp.loadSnapshot(snapshot, 'es-es'); // synchronous; false if it holds no catalog for the locale
|
|
491
|
+
createRoot(el).render(<App />); // the first render already has its translations
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
The snapshot is a cache, not the catalog of record: `init()` still fetches the catalog, which replaces it and supplies any phrase it lacked, and with no network anything it lacks shows its source text. Never edit a snapshot by hand — export it again. An edited file, a different format, an unsupported version or a missing member is refused with a `SnapshotError` whose `reason` says which.
|
|
495
|
+
|
|
496
|
+
## Write gating
|
|
497
|
+
|
|
498
|
+
A write-capable key shipped in public JavaScript is extractable, so **public keys are read-only**. The server decides per session whether that session may register newly-discovered phrases, and the SDK follows that decision — you never compute it client-side, because the same key can be write-enabled from one IP and read-only from another.
|
|
499
|
+
|
|
500
|
+
When a session is read-only, nothing is lost: the SDK reports the *page URL only* (never phrase content) so Langsys can visit that page from an allow-listed address and register what it finds.
|
|
501
|
+
|
|
502
|
+
### `useWriteEnabled()`
|
|
503
|
+
|
|
504
|
+
```tsx
|
|
505
|
+
const writeEnabled = useWriteEnabled(); // boolean | undefined
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Three distinct states — **don't collapse them to a boolean**:
|
|
509
|
+
|
|
510
|
+
| Value | Meaning |
|
|
511
|
+
|---|---|
|
|
512
|
+
| `undefined` | Authorization hasn't landed yet. Not the same as read-only. Also the value throughout SSR, since this is decided in the browser. |
|
|
513
|
+
| `false` | Read-only session. The SDK may report the page URL instead, subject to the key's auto-discovery permission. |
|
|
514
|
+
| `true` | This session registers content directly. |
|
|
515
|
+
|
|
516
|
+
Defaulting `undefined` to `false` will render a read-only state during every first paint and, under SSR, cause a hydration mismatch.
|
|
517
|
+
|
|
518
|
+
This package deliberately does **not** re-export the raw `writeEnabled` signal. It is `undefined` for the whole of a server render, so reading it directly during render defeats the guard `useWriteEnabled()` provides. If you genuinely need it outside React's render cycle, import it from `langsys-js-typescript` directly — the capability is available, it just isn't blessed under this binding's name.
|
|
519
|
+
|
|
520
|
+
### Write grants — for login-walled apps
|
|
521
|
+
|
|
522
|
+
Content behind a login can't be reached by an external visit, so pass a short-lived JWT your backend mints at login. The server then treats the session as write-enabled.
|
|
523
|
+
|
|
524
|
+
```tsx
|
|
525
|
+
LangsysApp.init({
|
|
526
|
+
projectid,
|
|
527
|
+
key,
|
|
528
|
+
UserLocaleStore: store,
|
|
529
|
+
// Prefer the function form: grants are short-lived (~5 min) and the SDK
|
|
530
|
+
// resolves it fresh before each request, so your auth layer decides when to
|
|
531
|
+
// mint. A static string expires while the app is still mounted.
|
|
532
|
+
writeGrant: () => auth.getLangsysGrant(),
|
|
533
|
+
});
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
If the token only exists *after* `init()` — the usual case — supply it when the user logs in:
|
|
537
|
+
|
|
538
|
+
```tsx
|
|
539
|
+
import { setWriteGrant } from 'langsys-js-react';
|
|
540
|
+
|
|
541
|
+
await setWriteGrant(token); // re-authorizes; resolves once the server has answered
|
|
542
|
+
await setWriteGrant(undefined); // clear on logout — session returns to read-only
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
`setWriteGrant()` re-authorizes rather than merely storing the token, so `useWriteEnabled()` reflects the new decision once it resolves. Misses occurring *after* the grant lands register directly; earlier ones were already reported through the URL-reporting path.
|
|
256
546
|
|
|
257
547
|
## Server-Side Rendering (Next.js, Remix)
|
|
258
548
|
|
|
259
|
-
|
|
549
|
+
Each server request renders inside a **request scope** of its own, with its own locale, catalog and misses, so concurrent visitors never see each other's language. `renderInRequestScope` from `langsys-js-react/server` opens one around a render. Once the server process has called `LangsysApp.init()`, `useT()`, `<Translate>` and `<Phrase>` render the request's locale on the server, so the served HTML is translated, apart from the blocks [README-SSR](./README-SSR.md#what-a-server-render-translates-and-discovers) lists as translated in the browser. The scope's seed, handed to the browser before hydration (`LangsysApp.seedCatalog`, or `<LangsysProvider seed>` in the Next App Router), makes the first client render match it.
|
|
260
550
|
|
|
261
|
-
📖 **See [README-SSR.md](./README-SSR.md)** for
|
|
551
|
+
📖 **See [README-SSR.md](./README-SSR.md)** for the walkthrough, including the Next.js App Router.
|
|
262
552
|
|
|
263
553
|
## Utilities
|
|
264
554
|
|
|
@@ -289,20 +579,46 @@ const supportedLocales = (await LangsysApp.getLocalesFlat()).map((l) => l.code);
|
|
|
289
579
|
const locale = LangsysApp.detectPreferredLocale(request.headers.get('Accept-Language'), supportedLocales);
|
|
290
580
|
```
|
|
291
581
|
|
|
292
|
-
The matcher tries exact match first (e.g. `en-US`), then language-only (`en` matches `en-GB`), and is script-aware via CLDR likely-subtags (base SDK 0.3.0+): `zh-TW` matches `zh-Hant` and never falls back to `zh-Hans`. Results are
|
|
582
|
+
The matcher tries exact match first (e.g. `en-US`), then language-only (`en` matches `en-GB`), and is script-aware via CLDR likely-subtags (base SDK 0.3.0+): `zh-TW` matches `zh-Hant` and never falls back to `zh-Hans`. Results are returned in the SDK's canonical form, which is **lowercase** (`en-us`, `zh-hant`) — see the note under [Initialization](#initialization); it returns `false` if no match.
|
|
583
|
+
|
|
584
|
+
### Asking your own API for the user's language
|
|
585
|
+
|
|
586
|
+
`localeHeaders()` returns the request header for the locale the user chose, so your app's calls to its own API answer in that language:
|
|
587
|
+
|
|
588
|
+
```ts
|
|
589
|
+
import { localeHeaders } from 'langsys-js-react';
|
|
590
|
+
|
|
591
|
+
fetch('/api/orders', { headers: { ...localeHeaders() } }); // Accept-Language: es-es
|
|
592
|
+
```
|
|
293
593
|
|
|
294
594
|
### Waiting for translations to load
|
|
295
595
|
|
|
296
|
-
When changing locale mid-session, you may want to re-run dependent code
|
|
596
|
+
When changing locale mid-session, you may want to re-run dependent code once the fetch ends:
|
|
297
597
|
|
|
298
598
|
```tsx
|
|
299
599
|
useEffect(() => {
|
|
300
600
|
LangsysApp.translationsLoadingPromise.then(() => {
|
|
301
|
-
// re-
|
|
601
|
+
// re-run dependent code here
|
|
302
602
|
});
|
|
303
603
|
}, [locale]);
|
|
304
604
|
```
|
|
305
605
|
|
|
606
|
+
`translationsLoadingPromise` settles when the catalog fetch **ends**, whether it succeeded or failed. It never rejects.
|
|
607
|
+
|
|
608
|
+
- **On success**, the new catalog is in place when it settles, so `t()` already returns the new locale's text. `useCurrentLocale()` follows about 100 ms later, so read it from a later render, not inside `.then()`.
|
|
609
|
+
- **On failure**, the page renders the source text, or the catalog stored for that locale from an earlier load. `useCurrentLocale()` keeps naming the last locale that loaded (`''` before any has) and never reaches the requested one, so a component that waits only for `useCurrentLocale() === requested` waits forever.
|
|
610
|
+
|
|
611
|
+
Treat a settled promise **and** `useCurrentLocale() === requested` as the only sign that the switch worked:
|
|
612
|
+
|
|
613
|
+
```tsx
|
|
614
|
+
// loaded -> settled && currentLocale === locale
|
|
615
|
+
// not yet -> anything else
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
A mismatch is not a failure test: right after a successful fetch settles, the locale has not been written yet. Nothing public tells a failed fetch apart from one that succeeded a moment ago, so if you need an error state, use your own timeout. The SDK logs a failed fetch to the console as an error.
|
|
619
|
+
|
|
620
|
+
After a failure, the SDK does not fetch that locale again for a backoff window (3 s, doubling with each consecutive failure up to 5 minutes); switching back to it inside the window renders what the failure did. `LangsysApp.refresh()` fetches at once; await the promise it returns, since it does not replace `translationsLoadingPromise`. Setting the locale store to the value it already holds does nothing.
|
|
621
|
+
|
|
306
622
|
## License
|
|
307
623
|
|
|
308
624
|
MIT © Langsys
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { LangsysApp as LangsysApp$1, iLangsysInitConfig as iLangsysInitConfig$1, Signal, iLangsysResponse } from 'langsys-js-typescript';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The entry point and its locale store, in a module that imports no React, so the main entry and
|
|
5
|
+
* `langsys-js-react/server` export the same `LangsysApp` and `createLocaleStore`. A server
|
|
6
|
+
* process, a Next Server Component included, initializes the SDK through the server entry
|
|
7
|
+
* without loading React's hooks.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* React-flavored init config. Identical to the base SDK's config except
|
|
12
|
+
* `UserLocaleStore` is typed as a `Signal<string>` — create one with
|
|
13
|
+
* `createLocaleStore()` (or get one from the `useLocaleStore` hook). The base
|
|
14
|
+
* SDK only reads and subscribes to it.
|
|
15
|
+
*/
|
|
16
|
+
interface iLangsysInitConfig extends Omit<iLangsysInitConfig$1, 'UserLocaleStore'> {
|
|
17
|
+
UserLocaleStore: Signal<string>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* React SDK entry point — the core singleton itself, re-exported **by
|
|
21
|
+
* reference**, with `init` typed to take the React-flavoured config.
|
|
22
|
+
*
|
|
23
|
+
* There is no wrapper and no list of delegated methods, so every public member
|
|
24
|
+
* of the core is reachable here the moment it exists, and identity is
|
|
25
|
+
* preserved: `LangsysApp` here **is** the core's `LangsysApp`. This binding
|
|
26
|
+
* overrides no behaviour, so the only thing it needs to express is a type.
|
|
27
|
+
*
|
|
28
|
+
* Destructuring behaves exactly as on the core. `const { t } = LangsysApp` is
|
|
29
|
+
* safe — `t` is a getter returning a closure. Prototype methods are not:
|
|
30
|
+
* `const { refresh } = LangsysApp` detaches the receiver and the call throws.
|
|
31
|
+
* Call methods on `LangsysApp` itself.
|
|
32
|
+
*
|
|
33
|
+
* The type narrows `init` to require a `Signal<string>` for `UserLocaleStore`.
|
|
34
|
+
* The core accepts the broader `LocaleSource`, which `Signal<string>`
|
|
35
|
+
* satisfies. Because the type is `Omit<typeof _LangsysApp, 'init'> & {…}` —
|
|
36
|
+
* keyof-mapped — it does not expose the core's private members: calling one is
|
|
37
|
+
* a compile error.
|
|
38
|
+
*
|
|
39
|
+
* Pinned by `src/surface.test.ts`.
|
|
40
|
+
*/
|
|
41
|
+
declare const LangsysApp: Omit<typeof LangsysApp$1, 'init'> & {
|
|
42
|
+
init(config: iLangsysInitConfig): Promise<iLangsysResponse>;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Create a reactive `Signal<string>` to hold the user's selected locale — the
|
|
46
|
+
* React analog of Svelte's `writable('en-US')`.
|
|
47
|
+
*
|
|
48
|
+
* Pass the result as `UserLocaleStore` to `LangsysApp.init`, read it reactively
|
|
49
|
+
* with `useSignal(store)` (or use the all-in-one `useLocaleStore` hook), and
|
|
50
|
+
* switch locale with `store.set('fr-FR')`. The base SDK only ever reads and
|
|
51
|
+
* subscribes to it — it never writes.
|
|
52
|
+
*
|
|
53
|
+
* Locale identifiers are canonicalized to BCP 47 by the base SDK (v0.3.0+), so
|
|
54
|
+
* `'en-us'` still works on input — but `currentlyLoadedLocale` always emits the
|
|
55
|
+
* canonical form (`'en-us'` — lowercase, per WIRE-3), so compare against the
|
|
56
|
+
* lowercase form. Verified against the core, not assumed: `canonicalizeLocale`
|
|
57
|
+
* lowercases both subtags, so a consumer testing `=== 'en-US'` never matches.
|
|
58
|
+
*/
|
|
59
|
+
declare function createLocaleStore(initial?: string): Signal<string>;
|
|
60
|
+
|
|
61
|
+
export { LangsysApp as L, createLocaleStore as c, type iLangsysInitConfig as i };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { LangsysApp as LangsysApp$1, iLangsysInitConfig as iLangsysInitConfig$1, Signal, iLangsysResponse } from 'langsys-js-typescript';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The entry point and its locale store, in a module that imports no React, so the main entry and
|
|
5
|
+
* `langsys-js-react/server` export the same `LangsysApp` and `createLocaleStore`. A server
|
|
6
|
+
* process, a Next Server Component included, initializes the SDK through the server entry
|
|
7
|
+
* without loading React's hooks.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* React-flavored init config. Identical to the base SDK's config except
|
|
12
|
+
* `UserLocaleStore` is typed as a `Signal<string>` — create one with
|
|
13
|
+
* `createLocaleStore()` (or get one from the `useLocaleStore` hook). The base
|
|
14
|
+
* SDK only reads and subscribes to it.
|
|
15
|
+
*/
|
|
16
|
+
interface iLangsysInitConfig extends Omit<iLangsysInitConfig$1, 'UserLocaleStore'> {
|
|
17
|
+
UserLocaleStore: Signal<string>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* React SDK entry point — the core singleton itself, re-exported **by
|
|
21
|
+
* reference**, with `init` typed to take the React-flavoured config.
|
|
22
|
+
*
|
|
23
|
+
* There is no wrapper and no list of delegated methods, so every public member
|
|
24
|
+
* of the core is reachable here the moment it exists, and identity is
|
|
25
|
+
* preserved: `LangsysApp` here **is** the core's `LangsysApp`. This binding
|
|
26
|
+
* overrides no behaviour, so the only thing it needs to express is a type.
|
|
27
|
+
*
|
|
28
|
+
* Destructuring behaves exactly as on the core. `const { t } = LangsysApp` is
|
|
29
|
+
* safe — `t` is a getter returning a closure. Prototype methods are not:
|
|
30
|
+
* `const { refresh } = LangsysApp` detaches the receiver and the call throws.
|
|
31
|
+
* Call methods on `LangsysApp` itself.
|
|
32
|
+
*
|
|
33
|
+
* The type narrows `init` to require a `Signal<string>` for `UserLocaleStore`.
|
|
34
|
+
* The core accepts the broader `LocaleSource`, which `Signal<string>`
|
|
35
|
+
* satisfies. Because the type is `Omit<typeof _LangsysApp, 'init'> & {…}` —
|
|
36
|
+
* keyof-mapped — it does not expose the core's private members: calling one is
|
|
37
|
+
* a compile error.
|
|
38
|
+
*
|
|
39
|
+
* Pinned by `src/surface.test.ts`.
|
|
40
|
+
*/
|
|
41
|
+
declare const LangsysApp: Omit<typeof LangsysApp$1, 'init'> & {
|
|
42
|
+
init(config: iLangsysInitConfig): Promise<iLangsysResponse>;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Create a reactive `Signal<string>` to hold the user's selected locale — the
|
|
46
|
+
* React analog of Svelte's `writable('en-US')`.
|
|
47
|
+
*
|
|
48
|
+
* Pass the result as `UserLocaleStore` to `LangsysApp.init`, read it reactively
|
|
49
|
+
* with `useSignal(store)` (or use the all-in-one `useLocaleStore` hook), and
|
|
50
|
+
* switch locale with `store.set('fr-FR')`. The base SDK only ever reads and
|
|
51
|
+
* subscribes to it — it never writes.
|
|
52
|
+
*
|
|
53
|
+
* Locale identifiers are canonicalized to BCP 47 by the base SDK (v0.3.0+), so
|
|
54
|
+
* `'en-us'` still works on input — but `currentlyLoadedLocale` always emits the
|
|
55
|
+
* canonical form (`'en-us'` — lowercase, per WIRE-3), so compare against the
|
|
56
|
+
* lowercase form. Verified against the core, not assumed: `canonicalizeLocale`
|
|
57
|
+
* lowercases both subtags, so a consumer testing `=== 'en-US'` never matches.
|
|
58
|
+
*/
|
|
59
|
+
declare function createLocaleStore(initial?: string): Signal<string>;
|
|
60
|
+
|
|
61
|
+
export { LangsysApp as L, createLocaleStore as c, type iLangsysInitConfig as i };
|