langsys-js-typescript 0.1.0 → 0.2.1

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 CHANGED
@@ -1,3 +1,13 @@
1
+ ## 0.2.1 - 2026-06-11
2
+
3
+ ### Changed
4
+
5
+ - **Migrated off the deprecated API routes onto the current endpoints.** No public API change — the same `t()`, `<Phrase>`, and `<Translate>` usage now talks to:
6
+ - Reads: `GET /translations?project_id=…&locale=…` (was the deprecated `GET /projects/{id}/translations`).
7
+ - Writes: `POST /translatable-items` with a unified `{ project_id, translatable_items: [{ type: 'phrase' | 'content_block', … }] }` body (was the deprecated `POST /projects/{id}/tokens` and `POST /projects/{id}/content-blocks`).
8
+ - Missing-phrase registration now chunks into batches of 200 to respect the `/translatable-items` per-request cap.
9
+ - Added `LangsysAppAPI.createTranslatableItems()` as the single write entry point for both phrases and content blocks.
10
+
1
11
  ## 0.1.0 - 2026-05-19
2
12
 
3
13
  Initial public release. Framework-agnostic TypeScript SDK for the Langsys Translation Manager — the foundation that `langsys-js-svelte` (and future framework-specific bindings) build on.
package/README.md CHANGED
@@ -9,7 +9,7 @@ This is the base SDK. Framework-specific bindings (`langsys-js-svelte`, and simi
9
9
  ## What's inside
10
10
 
11
11
  - `LangsysApp` singleton — init, locale switching, localized country/currency/locale helpers, preferred-locale detection.
12
- - `t(category, phrase, params?)` — the everyday translation function. The phrase is both the lookup key *and* the base-language default. Curly-brace placeholders interpolate from `params` and are compile-time type-checked.
12
+ - `t(phrase, category?, params?)` — the everyday translation function. The phrase is both the lookup key *and* the base-language default. Curly-brace placeholders interpolate from `params` and are compile-time type-checked.
13
13
  - `tSignal: Signal<TFunction>` — reactive primitive that re-emits a fresh `t` on every translations/locale change. Framework bindings subscribe to this to drive re-renders.
14
14
  - `Translate` class — wraps a DOM element, walks text + translatable attributes, registers a content block with the Translation Manager, re-translates on locale change.
15
15
  - `Signal<T>` — tiny subscribe/set/update/get primitive. Compatible with Svelte's store contract by design, so the Svelte binding is nearly trivial.
@@ -42,13 +42,13 @@ const response = await LangsysApp.init({
42
42
 
43
43
  if (!response.status) console.error('Langsys init failed', response.errors);
44
44
 
45
- // 3. Translate
46
- document.querySelector('h1')!.textContent = t('Home', 'Welcome to my app');
47
- document.querySelector('#tagline')!.textContent = t('Marketing', 'We translate everything');
45
+ // 3. Translate — t(phrase, category?, params?)
46
+ document.querySelector('h1')!.textContent = t('Welcome to my app', 'Home');
47
+ document.querySelector('#tagline')!.textContent = t('We translate everything', 'Marketing');
48
48
 
49
49
  // 4. Interpolate
50
50
  document.querySelector('#greeting')!.textContent =
51
- t('Greetings', 'Hello, {name}!', { name: 'Sarah' });
51
+ t('Hello, {name}!', 'Greetings', { name: 'Sarah' });
52
52
 
53
53
  // 5. Change locale — all subscribed consumers re-translate
54
54
  userLocale.set('es-es');
@@ -56,27 +56,27 @@ userLocale.set('es-es');
56
56
 
57
57
  ### How `t()` works
58
58
 
59
- The phrase is the lookup key. The first time `t('Home', 'Welcome to my app')` runs with a write key, Langsys registers the phrase in the Translation Manager under the "Home" category. On subsequent locale changes, the SDK fetches translations and `t()` returns the translated version. If no translation exists yet, you get back the original phrase — your base language stays visible while translations get filled in.
59
+ The signature is `t(phrase, category?, params?)` — the phrase comes first, the category is optional, params come last. The phrase is the lookup key. The first time `t('Welcome to my app', 'Home')` runs with a write key, Langsys registers the phrase in the Translation Manager under the "Home" category. On subsequent locale changes, the SDK fetches translations and `t()` returns the translated version. If no translation exists yet, you get back the original phrase — your base language stays visible while translations get filled in.
60
60
 
61
61
  #### Compile-time-checked interpolation
62
62
 
63
63
  Placeholder names are extracted from the phrase string literal via template-literal types, so missing or extra keys in `params` are TypeScript errors at the call site:
64
64
 
65
65
  ```ts
66
- t('Greetings', 'Hello, {name}!', { name: 'Sarah' }); // OK
67
- t('Greetings', 'Hello, {name}!', {}); // ❌ Property 'name' missing
68
- t('Greetings', 'Hello, {name}!', { name: 'x', extra: 'y' }); // ❌ Unknown property 'extra'
66
+ t('Hello, {name}!', 'Greetings', { name: 'Sarah' }); // OK
67
+ t('Hello, {name}!', 'Greetings', {}); // ❌ Property 'name' missing
68
+ t('Hello, {name}!', 'Greetings', { name: 'x', extra: 'y' }); // ❌ Unknown property 'extra'
69
69
  ```
70
70
 
71
71
  Allowed value types: `string | number | Date | boolean`. Dates serialize to ISO 8601.
72
72
 
73
- > Future versions will swap the simple `{name}` runtime for ICU MessageFormat — adding plural / select / date formatting — without changing the public signature. Today's `t('Cart', '{count} items', { count })` will evolve to `t('Cart', '{count, plural, one {# item} other {# items}}', { count })`.
73
+ > Future versions will swap the simple `{name}` runtime for ICU MessageFormat — adding plural / select / date formatting — without changing the public signature. Today's `t('{count} items', 'Cart', { count })` will evolve to `t('{count, plural, one {# item} other {# items}}', 'Cart', { count })`.
74
74
 
75
75
  #### Categorization disambiguates context
76
76
 
77
77
  ```ts
78
- t('Main Menu', 'Home'); // 'Inicio' in Spanish
79
- t('Home repairs', 'Home'); // 'Hogar' in Spanish
78
+ t('Home', 'Main Menu'); // 'Inicio' in Spanish
79
+ t('Home', 'Home repairs'); // 'Hogar' in Spanish
80
80
  ```
81
81
 
82
82
  The same phrase in different categories can have different translations. Without categorization, "Home" would only have one. Langsys's philosophy: *translate once, use everywhere* — categorize when the same phrase legitimately means different things in different parts of the app.
@@ -101,7 +101,7 @@ import { tSignal } from 'langsys-js-typescript';
101
101
 
102
102
  const unsub = tSignal.subscribe((t) => {
103
103
  // t is the current translation function — call it for the latest translations
104
- document.querySelector('h1')!.textContent = t('Home', 'Welcome');
104
+ document.querySelector('h1')!.textContent = t('Welcome', 'Home');
105
105
  });
106
106
  ```
107
107
 
@@ -122,11 +122,11 @@ export function useT(): TFunction {
122
122
  // In a component:
123
123
  function Header() {
124
124
  const t = useT();
125
- return <h1>{t('Home', 'Welcome, {name}!', { name: 'Sarah' })}</h1>;
125
+ return <h1>{t('Welcome, {name}!', 'Home', { name: 'Sarah' })}</h1>;
126
126
  }
127
127
  ```
128
128
 
129
- When a `langsys-js-react` package exists, it'll ship that hook plus a `<TranslateBlock>` wrapper around the DOM `Translate` class. Until then, the snippet above is the whole binding.
129
+ The dedicated [`langsys-js-react`](https://github.com/langsys/langsys-js-react) package ships that hook (as `useT`) plus a `<Translate>` component around the DOM `Translate` class. The snippet above is the whole binding if you'd rather not add the dependency.
130
130
 
131
131
  ### Svelte
132
132
 
@@ -195,7 +195,7 @@ const locale = LangsysApp.detectPreferredLocale(
195
195
  );
196
196
  ```
197
197
 
198
- The matcher tries exact match first (`en-US`), then language-only (`en` matches `en-GB`), then returns `null`.
198
+ The matcher tries exact match first (`en-US`), then language-only (`en` matches `en-GB`). When you pass `supported` and none match, it falls back to the user's top preference (normalized); it returns `false` only when no preference can be determined at all.
199
199
 
200
200
  ## Localized country / currency / locale lists
201
201
 
@@ -261,7 +261,7 @@ import type {
261
261
  - `LangsysApp.getCurrencies(inLocale?)` / `.getCurrencyName(code, inLocale?)`
262
262
  - `LangsysApp.getLocales(inLocale?)` / `.getLocalesFlat(inLocale?)` / `.getLocalesData(inLocale?, force?)`
263
263
  - `LangsysApp.getLocaleName(code, short?, inLocale?)` / `.getLocaleNameWithLookup(...)`
264
- - Top-level: `t`, `tSignal`, `currentlyLoadedLocale`, `sTranslations`, `contentBlocks`, `LangsysAppAPI`, `Translate`, `createSignal`, `getValue`, `persist`, `interpolate`, `Logger`, `logger`, `md5`, `isEmpty`.
264
+ - Top-level: `t`, `tSignal`, `currentlyLoadedLocale`, `sTranslations`, `LangsysAppAPI`, `Translate`, `createSignal`, `getValue`, `persist`, `interpolate`, `Logger`, `logger`, `md5`, `isEmpty`.
265
265
 
266
266
  ## License
267
267
 
package/dist/index.d.mts CHANGED
@@ -60,17 +60,37 @@ type iCategories = {
60
60
  __uncategorized__: iTranslations;
61
61
  };
62
62
 
63
+ /**
64
+ * Minimum interface for a user-locale source.
65
+ *
66
+ * The SDK only ever READS and SUBSCRIBES to this — never writes. That lets
67
+ * each framework adapt its native state into this shape with a small adapter:
68
+ *
69
+ * - Svelte: a `writable<string>` satisfies this directly.
70
+ * - React: wrap `useState` via `useSyncExternalStore` — a 5-line adapter.
71
+ * - Vue: wrap `ref<string>` via `watch` + a `.value` getter.
72
+ * - Angular: a `BehaviorSubject<string>` satisfies this directly.
73
+ * - Vanilla: use the SDK's own `createSignal<string>('en-us')`.
74
+ *
75
+ * Designed as the smallest possible contract so framework wrappers stay thin.
76
+ */
77
+ interface LocaleSource {
78
+ /** Read the current locale synchronously. */
79
+ get(): string;
80
+ /** Register a callback for locale changes. Returns an unsubscribe function. */
81
+ subscribe(run: (locale: string) => void): () => void;
82
+ }
63
83
  interface iLangsysInitConfig {
64
84
  /** The ID (UUID) of the project created in Langsys */
65
85
  projectid: string;
66
86
  /** The API key associated with the project */
67
87
  key: string;
68
88
  /**
69
- * A reactive holder for the user's selected locale. Accepts any object
70
- * with `subscribe`, `set`, and `get` methods (our `Signal<string>` works
71
- * out of the box). Use `createSignal('en-us')` if you have nothing of your own.
89
+ * A reactive source for the user's selected locale. See `LocaleSource`
90
+ * for the minimum contract. The SDK's own `createSignal('en-us')` works
91
+ * out of the box if you don't have a framework-native store of your own.
72
92
  */
73
- UserLocaleStore: Signal<string>;
93
+ UserLocaleStore: LocaleSource;
74
94
  /**
75
95
  * Base language/locale for the source strings in your code.
76
96
  * @default 'en'
@@ -102,7 +122,7 @@ interface iLangsysInitConfig {
102
122
  interface iLangsysConfig {
103
123
  projectid: string;
104
124
  key: string;
105
- sUserLocale: Signal<string>;
125
+ sUserLocale: LocaleSource;
106
126
  baseLocale: string;
107
127
  debug?: boolean;
108
128
  /** Permission level of the API key, set automatically after successful authorization. */
@@ -122,7 +142,7 @@ interface iLangsysConfig {
122
142
  *
123
143
  * The big trick here is `ExtractParamKeys` — a recursive template-literal
124
144
  * type that pulls placeholder names out of a phrase at compile time, so
125
- * `t('Cat', 'Hello, {name}!', { name: 'Sarah' })` is type-checked: passing
145
+ * `t('Hello, {name}!', { name: 'Sarah' })` is type-checked: passing
126
146
  * an object without `name`, or with a key not in the phrase, is a TS error.
127
147
  */
128
148
  /** Allowed value types for interpolation. Mirrors what ICU MessageFormat will accept. */
@@ -147,9 +167,22 @@ type ParamsFor<S extends string> = {
147
167
  type TArgs<P extends string> = [ExtractParamKeys<P>] extends [never] ? [] : [params: ParamsFor<P>];
148
168
  /** Loose params type — used inside generic stores where the phrase isn't a known literal. */
149
169
  type TranslationParams = Record<string, ParamPrimitive>;
150
- /** The translation function signature. */
170
+ /**
171
+ * The translation function signature.
172
+ *
173
+ * t('Save') // no category, no params
174
+ * t('Save', 'UI') // categorized
175
+ * t('Hello, {name}!', { name: 'X' }) // no category, with params
176
+ * t('Hello, {name}!', 'Greetings', { name: 'X' }) // category + params
177
+ *
178
+ * Two overloads discriminate on the type at position 2 (string → category,
179
+ * object → params). `category` is required for the second overload —
180
+ * pass any non-empty string. There is no "null category" wire concept:
181
+ * absent category means absent.
182
+ */
151
183
  interface TFunction {
152
- <P extends string>(category: string, phrase: P, ...args: TArgs<P>): string;
184
+ <P extends string>(phrase: P, ...args: TArgs<P>): string;
185
+ <P extends string>(phrase: P, category: string, ...args: TArgs<P>): string;
153
186
  }
154
187
 
155
188
  /**
@@ -170,10 +203,20 @@ declare class Translations {
170
203
  private timer;
171
204
  private flushScheduled;
172
205
  private isFirstClientRun;
206
+ private readyResolve;
207
+ private readyPromise;
173
208
  debug: Logger;
174
209
  /** Reactive holder for the current translation function. Re-emits on every catalog/locale change. */
175
210
  tSignal: Signal<TFunction>;
176
211
  constructor(config: iLangsysConfig);
212
+ /**
213
+ * Resolves once the first `getTranslations` call has completed (or
214
+ * immediately if no fetch is configured). Callers that need to read
215
+ * `sTranslations` before issuing a write — e.g. the `Translate` DOM
216
+ * class deciding whether to POST a content block — should await this
217
+ * to avoid acting on an empty cache during the cold-start race.
218
+ */
219
+ ready(): Promise<void>;
177
220
  /** Current translation function. Reads fresh state on every call. */
178
221
  get t(): TFunction;
179
222
  /** Subscribe to translation snapshot changes. Fires immediately with the current value. */
@@ -185,6 +228,13 @@ declare class Translations {
185
228
  * a missing token — the DOM class manages its own lifecycle via content blocks.
186
229
  */
187
230
  lookupContent(category: string, customId: string, token: string): string | null;
231
+ /**
232
+ * Raw stored translation for a phrase, without interpolation or
233
+ * missing-token registration. Returns null when no (non-empty) translation
234
+ * is cached. Used by the `Phrase` rich-text handler, which needs the
235
+ * un-interpolated template (it supplies its own markup-token values).
236
+ */
237
+ lookup(phrase: string, category: string): string | null;
188
238
  private buildTFn;
189
239
  private missingToken;
190
240
  private scheduleTokenFlush;
@@ -273,7 +323,8 @@ declare class LangsysAppClass {
273
323
  refresh(): Promise<boolean>;
274
324
  /**
275
325
  * Initialize Langsys. Configuration must include at least `projectid`,
276
- * `key`, and `UserLocaleStore` (any Signal<string>).
326
+ * `key`, and `UserLocaleStore` (anything satisfying `LocaleSource` — see
327
+ * its docstring for framework-specific adapters).
277
328
  */
278
329
  init(initConfig: iLangsysInitConfig): Promise<ResponseObject>;
279
330
  getDialCodes(inLocale?: string): Promise<iCountryDialCode[]>;
@@ -329,7 +380,6 @@ interface TranslateOptions {
329
380
  declare class Translate {
330
381
  private element;
331
382
  private options;
332
- private contentClone;
333
383
  private tokens;
334
384
  private parseComplete;
335
385
  private isTokenizing;
@@ -342,17 +392,115 @@ declare class Translate {
342
392
  private translateUpdate;
343
393
  private tokenizeContent;
344
394
  private handleContentBlock;
345
- private tokenize;
346
- private tokenizeAttributes;
347
- private findNode;
348
395
  private translate;
349
396
  private getTranslation;
350
397
  private translateAttributes;
351
398
  private translateAttribute;
352
- private generateCustomId;
353
- private applyStylesToNode;
354
399
  }
355
400
 
401
+ /** Attribute marker the `Translate` tokenizer uses to skip a `<Phrase>` subtree. */
402
+ declare const PHRASE_MARKER_ATTR = "data-ls-phrase";
403
+ interface PhraseOptions {
404
+ /** Category the phrase registers under (disambiguation for translators). */
405
+ category?: string;
406
+ /** Interpolation params — `{n}` for pluralization, `{name}`, etc. */
407
+ params?: Record<string, unknown>;
408
+ }
409
+ /**
410
+ * DOM handler for a markup-bearing "rich" phrase (`<Phrase>`).
411
+ *
412
+ * Wraps an element whose inner content is ONE translatable phrase even though
413
+ * it contains inline markup. On mount it encodes the subtree to a single phrase
414
+ * string with neutral `{mNo}`/`{mNc}` markup tokens (the real elements stay in
415
+ * the SDK — see richtext.ts), registers it, and renders the translation by
416
+ * reconstituting the original elements around the translated text. Re-renders
417
+ * on locale change and whenever params change.
418
+ *
419
+ * new Phrase(el, { category: 'ProductCard', params: { n: reviewCount } });
420
+ */
421
+ declare class Phrase {
422
+ private host;
423
+ private category;
424
+ private params;
425
+ private phrase;
426
+ private slots;
427
+ private unsubscribers;
428
+ private ready;
429
+ constructor(host: HTMLElement, options?: PhraseOptions);
430
+ /** Update interpolation params (e.g. a changed count) and re-render. */
431
+ setParams(params: Record<string, unknown>): void;
432
+ /** Stop reacting to locale/translation changes. Safe to call multiple times. */
433
+ destroy(): void;
434
+ private _init;
435
+ private _render;
436
+ }
437
+
438
+ /**
439
+ * Rich-text (markup-bearing) phrase encoding for the `<Phrase>` artifact.
440
+ *
441
+ * The problem: a phrase like `You have {n} <strong>reviews</strong>` must stay
442
+ * ONE phrase (so pluralization sees `{n}` next to `reviews`), but we must NOT
443
+ * send the SPA's real markup — and especially not framework-internal scoped-CSS
444
+ * classes (`svelte-a1b2c3`, Vue scoped hashes) — to the translator. Those
445
+ * classes get mangled, and worse they change on every build, which would make
446
+ * the phrase key drift and silently re-translate everything.
447
+ *
448
+ * The approach: replace each inline element with a pair of neutral MARKUP
449
+ * TOKENS — `{m0o}` ... `{m0c}` (markup, slot 0, open/close). These are ordinary
450
+ * ICU placeholders: valid MessageFormat argument names, so plural/select still
451
+ * parse, and the model treats them like `{n}` — it preserves them and PLACES
452
+ * them around the translated word (which is what fixes reordering languages,
453
+ * e.g. `<span>White</span> House` -> `Casa <span>Blanca</span>`).
454
+ *
455
+ * The real DOM elements never leave the SDK. At render time we resolve the
456
+ * translation, substitute each markup token with a private-use SENTINEL char
457
+ * (transient — never sent anywhere), then reconstitute the DOM by wrapping the
458
+ * sentinel-delimited spans in the ORIGINAL framework-owned elements. Scoped CSS
459
+ * survives because we reuse the real element; the phrase key is stable because
460
+ * the wire form only ever contains `{m0o}`/`{m0c}`, never build-specific hashes.
461
+ */
462
+ /** A captured inline element: a shallow clone (tag + attributes, no children). */
463
+ interface RichSlot {
464
+ /** Shallow clone of the original element — preserves tag, class, href, etc. */
465
+ template: HTMLElement;
466
+ }
467
+ interface EncodedRichText {
468
+ /**
469
+ * The phrase string with inline elements replaced by `{mNo}`...`{mNc}` markup
470
+ * tokens. This is what gets registered + sent for translation.
471
+ */
472
+ phrase: string;
473
+ /** Captured inline elements, indexed by markup-token slot number. */
474
+ slots: RichSlot[];
475
+ }
476
+ /**
477
+ * Walk an element's subtree and produce the encoded phrase + captured slots.
478
+ *
479
+ * Text nodes contribute their (whitespace-collapsed) text. Each child element
480
+ * becomes a markup-token pair wrapping its recursively-encoded contents, and a
481
+ * shallow clone of the element is captured as a slot. Nesting is preserved.
482
+ */
483
+ declare function encodeRichText(root: HTMLElement): EncodedRichText;
484
+ /**
485
+ * The param values to feed alongside the user's params when resolving a rich
486
+ * phrase. Each markup token maps to its sentinel pair so the resolved string
487
+ * carries machine-findable delimiters for reconstitution.
488
+ */
489
+ declare function markupTokenValues(slotCount: number): Record<string, string>;
490
+ /**
491
+ * Rebuild DOM nodes from a resolved string containing sentinel-delimited spans.
492
+ *
493
+ * The original (framework-owned) elements are reused as wrappers — so scoped
494
+ * CSS classes / attributes survive — with the translated text placed inside.
495
+ *
496
+ * Robust to model mistakes: if sentinels are unbalanced (a tag dropped or a
497
+ * stray close), we fall back to a single plain-text node (markers stripped) —
498
+ * "lose the markup, keep the meaning" rather than throw.
499
+ */
500
+ declare function reconstitute(resolved: string, slots: RichSlot[], doc?: Document): Node[];
501
+ /** Remove any sentinel markers from a string, leaving the bare text. */
502
+ declare function stripSentinels(value: string): string;
503
+
356
504
  declare class LangsysAppAPIClass {
357
505
  private apiurl;
358
506
  config: iLangsysConfig;
@@ -363,6 +511,12 @@ declare class LangsysAppAPIClass {
363
511
  setBaseUrl(url: string): void;
364
512
  validate(config: iLangsysConfig): Promise<ResponseObject>;
365
513
  getTranslations(locale: string): Promise<ResponseObject>;
514
+ /**
515
+ * Register phrases and/or content blocks via the current unified endpoint.
516
+ * Each item carries `type: 'phrase' | 'content_block'`. Replaces the
517
+ * deprecated `projects/{id}/tokens` and `projects/{id}/content-blocks` routes.
518
+ */
519
+ createTranslatableItems(items: Array<Record<string, unknown>>): Promise<ResponseObject>;
366
520
  post(path: string, data?: Record<string, unknown>): Promise<ResponseObject>;
367
521
  get(path: string, data?: Record<string, unknown>): Promise<ResponseObject>;
368
522
  delete(path: string, data?: Record<string, unknown>): Promise<ResponseObject>;
@@ -372,13 +526,6 @@ declare class LangsysAppAPIClass {
372
526
  }
373
527
  declare const LangsysAppAPI: LangsysAppAPIClass;
374
528
 
375
- /**
376
- * Create a Signal whose value is persisted to localStorage (when available).
377
- * Falls back to in-memory behavior during SSR or when storage is unavailable
378
- * or quota-exceeded. Stored value is JSON-encoded.
379
- */
380
- declare function persist<T>(key: string, initial: T): Signal<T>;
381
-
382
529
  interface iContentBlock {
383
530
  custom_id: string;
384
531
  category: string;
@@ -387,23 +534,139 @@ interface iContentBlock {
387
534
  tokens: string[];
388
535
  }
389
536
 
537
+ /**
538
+ * Framework-agnostic content-block helpers.
539
+ *
540
+ * The DOM-mutating `Translate` class in `translate.ts` is one consumer of
541
+ * these — it walks a live element, tokenizes it, registers the content
542
+ * block, and mutates the same element in place. That model fits Svelte
543
+ * and vanilla JS, where the SDK genuinely owns the DOM node.
544
+ *
545
+ * For React/Vue/Angular wrappers, mutating the DOM directly is an anti-
546
+ * pattern — those frameworks own their rendered output. Wrappers should
547
+ * import the helpers below to handle the framework-agnostic parts (token
548
+ * discovery, content-block registration, dedup against the local cache)
549
+ * and render translated content through their framework's native means.
550
+ *
551
+ * Wrapper pattern:
552
+ *
553
+ * 1. After mount, grab the rendered DOM via the framework's ref/binding.
554
+ * 2. `const { tokens, content } = tokenizeElement(el);`
555
+ * 3. `const customId = generateCustomId(category, tokens);`
556
+ * 4. `if (!isContentBlockKnown(category, customId)) {`
557
+ * ` await registerContentBlock({ custom_id: customId, category, content, tokens, label });`
558
+ * `}`
559
+ * 5. Subscribe to `sTranslations` + `currentlyLoadedLocale`; on change,
560
+ * look up each token via `Translations.lookupContent(category, customId, token)`
561
+ * and render via your framework's templating.
562
+ */
563
+
564
+ /**
565
+ * Compute the deterministic content-block id from a `(category, tokens)` pair.
566
+ *
567
+ * JSON-stringifies the tuple so each token is unambiguously delimited
568
+ * (the previous `tokens.join('-')` was collision-prone for tokens
569
+ * containing a hyphen, e.g. `'e-mail'`).
570
+ *
571
+ * Pure function — same inputs always produce the same id. Exported so
572
+ * framework wrappers can compute the id themselves before deciding
573
+ * whether to call `registerContentBlock`.
574
+ */
575
+ declare function generateCustomId(category: string, tokens: string[]): string;
576
+ /**
577
+ * True iff the local translations cache already has the content block
578
+ * for the given `(category, customId)` — meaning the backend knows about
579
+ * it and there's no need to re-register.
580
+ *
581
+ * The cache is keyed under `'__uncategorized__'` for null/empty categories,
582
+ * matching the convention the server uses in its GET /translations response.
583
+ */
584
+ declare function isContentBlockKnown(category: string, customId: string): boolean;
585
+ /**
586
+ * POST a content block to the backend, then stamp its custom_id into the
587
+ * local translations cache so subsequent mounts in this session — and
588
+ * subsequent reloads, since the cache is persisted — skip the POST.
589
+ *
590
+ * Mirrors the standalone-phrase path's cache writeback semantics. Respects
591
+ * `key_type === 'write'`: read-only keys silently no-op (resolves with
592
+ * `{ status: true }` so the caller can still render the cached translation).
593
+ *
594
+ * Errors are logged via the SDK's logger and returned as `{ status: false }`
595
+ * — never thrown. Callers can decide whether to surface to UI or swallow.
596
+ */
597
+ declare function registerContentBlock(contentBlock: iContentBlock): Promise<{
598
+ status: boolean;
599
+ errors?: unknown[];
600
+ }>;
601
+ /**
602
+ * Walk an `HTMLElement`, collecting tokens for translation and producing a
603
+ * cloned-and-stylized snapshot of its outerHTML for the Translation Manager.
604
+ *
605
+ * Tokens harvested:
606
+ * - text-node content (whitespace-normalized, trimmed)
607
+ * - translatable attribute values (`placeholder`, `alt`, `title`, `aria-*`, etc.)
608
+ * - button/input/select value text
609
+ *
610
+ * Skips subtrees marked with `translate="no"`.
611
+ *
612
+ * The returned `content` is a string snapshot — the live element is not
613
+ * mutated. Computed semantic styles (color, font, spacing, etc.) are inlined
614
+ * onto the clone so translators see content styled the way end-users do.
615
+ *
616
+ * Requires that `element` is mounted in the document so `getComputedStyle`
617
+ * can read its style. For SSR / pre-mount callers, the style capture step
618
+ * silently no-ops and the snapshot is plain (without inlined styles).
619
+ */
620
+ declare function tokenizeElement(element: HTMLElement): {
621
+ tokens: string[];
622
+ content: string;
623
+ };
624
+
625
+ /**
626
+ * Create a Signal whose value is persisted to localStorage (when available).
627
+ * Falls back to in-memory behavior during SSR or when storage is unavailable
628
+ * or quota-exceeded. Stored value is JSON-encoded.
629
+ */
630
+ declare function persist<T>(key: string, initial: T): Signal<T>;
631
+
390
632
  declare const sTranslations: Signal<iCategories>;
391
633
  declare const currentlyLoadedLocale: Signal<string>;
392
- declare const contentBlocks: Signal<iContentBlock[]>;
393
634
 
394
635
  declare const isEmpty: (data: unknown) => boolean;
395
636
  declare function md5(inputString: string): string;
396
637
 
397
638
  /**
398
- * Substitute `{name}`-style placeholders in a translated string with values
399
- * from `params`. Unknown placeholders are left untouched so missing data is
400
- * visible to the developer rather than silently rendering empty strings.
639
+ * Detects ICU MessageFormat syntax in a translation string.
640
+ *
641
+ * - Matches `{var, plural, …}`, `{var, select, …}`, `{var, selectordinal, …}`,
642
+ * `{var, number, …}`, `{var, date, …}`, `{var, time, …}`.
643
+ * - Doesn't match plain interpolation slots like `{name}` or `{count}`.
644
+ *
645
+ * Exported because the Translation Manager UI needs the same detection logic
646
+ * to decide whether to render a plural-slot editor vs. a single text field.
647
+ */
648
+ declare function isICU(template: string): boolean;
649
+ /**
650
+ * Substitute placeholders in a translated string with values from `params`.
651
+ *
652
+ * Two paths:
653
+ * - ICU MessageFormat (target string contains `{var, plural|select|…, …}`):
654
+ * parsed and formatted via `intl-messageformat`, which knows every
655
+ * target locale's plural rules (Arabic's 6 categories, Russian's 4,
656
+ * etc.) and gender-select branches.
657
+ * - Simple `{name}` interpolation: cheap regex replacement. Unknown
658
+ * placeholders are left untouched so missing data is visible to the
659
+ * developer rather than silently rendering empty strings.
660
+ *
661
+ * Malformed ICU falls through to simple interpolation rather than throwing,
662
+ * so the developer sees a broken-but-visible string instead of a runtime crash.
663
+ * Backend-side validation in langsys-ai is supposed to prevent this — the
664
+ * fall-through is defense in depth.
401
665
  *
402
- * Dates are serialized to ISO 8601; everything else goes through `String()`.
403
- * Designed as a forward-compatible stop-gap until ICU MessageFormat is wired
404
- * in for plurals/select/date formatting.
666
+ * `locale` is required for ICU formatting (drives plural-rule selection).
667
+ * Falls back to `'en'` if absent — simple-interpolation path ignores it.
405
668
  */
406
- declare function interpolate(template: string, params: Record<string, unknown>): string;
669
+ declare function interpolate(template: string, params: Record<string, unknown>, locale?: string): string;
407
670
 
408
671
  interface iProject {
409
672
  id: string;
@@ -425,4 +688,4 @@ interface iProject {
425
688
  declare const t: TFunction;
426
689
  declare const tSignal: Signal<TFunction>;
427
690
 
428
- export { type ExtractParamKeys, LangsysApp, LangsysAppAPI, Logger, type ParamPrimitive, type ParamsFor, type Signal, type Subscriber, type TArgs, type TFunction, Translate, type TranslateOptions, type TranslationParams, Translations, type Unsubscriber, type Updater, contentBlocks, createSignal, currentlyLoadedLocale, getValue, type iCategories, type iContentBlock, type iCountry, type iCountryDialCode, type iCountryList, type iCurrency, type iCurrencyList, type iLangsysConfig, type iLangsysInitConfig, type ResponseObject as iLangsysResponse, type iLanguageName, type iLocaleData, type iLocaleDefault, type iLocaleFlat, type iProject, type iTranslations, interpolate, isEmpty, logger, md5, persist, sTranslations, t, tSignal };
691
+ export { type EncodedRichText, type ExtractParamKeys, LangsysApp, LangsysAppAPI, type LocaleSource, Logger, PHRASE_MARKER_ATTR, type ParamPrimitive, type ParamsFor, Phrase, type PhraseOptions, type RichSlot, type Signal, type Subscriber, type TArgs, type TFunction, Translate, type TranslateOptions, type TranslationParams, Translations, type Unsubscriber, type Updater, createSignal, currentlyLoadedLocale, encodeRichText, generateCustomId, getValue, type iCategories, type iContentBlock, type iCountry, type iCountryDialCode, type iCountryList, type iCurrency, type iCurrencyList, type iLangsysConfig, type iLangsysInitConfig, type ResponseObject as iLangsysResponse, type iLanguageName, type iLocaleData, type iLocaleDefault, type iLocaleFlat, type iProject, type iTranslations, interpolate, isContentBlockKnown, isEmpty, isICU, logger, markupTokenValues, md5, persist, reconstitute, registerContentBlock, sTranslations, stripSentinels, t, tSignal, tokenizeElement };