langsys-js-typescript 0.1.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 +31 -0
- package/LICENSE +21 -0
- package/README.md +268 -0
- package/dist/index.d.mts +428 -0
- package/dist/index.d.ts +428 -0
- package/dist/index.js +1249 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1231 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +56 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
## 0.1.0 - 2026-05-19
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`t(category, phrase, params?)`** — the everyday translation function. Phrase is both lookup key and base-language default. Curly-brace placeholders are interpolated from `params`.
|
|
8
|
+
- **Compile-time-checked interpolation** via template-literal types (`ExtractParamKeys<S>`, `ParamsFor<S>`, `TArgs<P>`). Missing or extra params keys are TypeScript errors at the call site.
|
|
9
|
+
- **`tSignal: Signal<TFunction>`** — reactive primitive that re-emits a fresh `t` on every translations/locale change. Framework bindings subscribe to this for re-render invalidation.
|
|
10
|
+
- **`Signal<T>`** — minimal subscribe/set/update/get observable with Svelte-store-compatible semantics. Used internally and exposed for consumers.
|
|
11
|
+
- **`Translate` DOM class** — wraps an HTMLElement, walks text nodes + translatable attributes (`placeholder`, `alt`, `title`, `aria-label`, `aria-placeholder`, button/input `value`, `<option>` text, several validation `data-*` attrs), registers content blocks with the Translation Manager, re-translates on locale change. `translate="no"` opts elements out.
|
|
12
|
+
- **`LangsysApp` singleton** — `init`, `refresh`, `translationsLoadingPromise`, localized list helpers (`getCountries`, `getDialCodes`, `getCurrencies`, `getLocales*`, `getCountryName`, `getCurrencyName`, `getLocaleName`, `getLocaleNameWithLookup`).
|
|
13
|
+
- **`LangsysAppAPI`** — direct HTTP access if you want to bypass the rest.
|
|
14
|
+
- **SSR support** — `initialTranslations` / `initialTranslationsLocale` to seed pre-fetched data, `ssrTokenStrategy: 'client' | 'server' | 'auto'` to control when missing tokens are flushed.
|
|
15
|
+
- **API key permission auto-detection** — read-only keys skip token creation; write keys auto-register new phrases and content blocks.
|
|
16
|
+
- **`detectPreferredLocale(acceptLanguageHeader?, supportedLocales?)`** — browser (`navigator.languages`) and SSR (`Accept-Language`) detection with optional locale matching (exact → language-only → null).
|
|
17
|
+
- **`persist<T>(key, initial)`** — Signal seeded from localStorage with SSR-safe fallback to in-memory.
|
|
18
|
+
- **`interpolate(template, params)`** — exposed for advanced use cases; handles `{name}` placeholders, Date → ISO 8601 serialization.
|
|
19
|
+
- **`Logger` / `logger`** — debug-gated console output with styled grouping in browsers, plain output in Node.
|
|
20
|
+
- **`md5`**, **`isEmpty`** — utilities used internally, exported for reuse.
|
|
21
|
+
- **Vanilla example** in `example/index.html` — single-file demo showing `LangsysApp.init` and `new Translate(...)` with no framework dependencies.
|
|
22
|
+
|
|
23
|
+
### Notes
|
|
24
|
+
|
|
25
|
+
- Zero runtime dependencies. Bundled with [tsup](https://tsup.egoel.dev/) as ESM + CJS + `.d.ts`.
|
|
26
|
+
- Targets ES2021. Works in modern browsers, Node ≥18, any bundler.
|
|
27
|
+
- Build sizes: ~44 KB ESM, ~44 KB CJS, ~16 KB d.ts.
|
|
28
|
+
|
|
29
|
+
### Provenance
|
|
30
|
+
|
|
31
|
+
This package replaces the unfinished port of `langsys-js-svelte` that previously occupied this directory. The architecture is new: the proxy-based `_['Cat']['Token']` access pattern from the Svelte SDK was deliberately replaced with the function-call `t('Cat', 'Phrase', params?)` form so interpolation and (future) ICU MessageFormat plurals fit naturally.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Langsys
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Langsys SDK — TypeScript
|
|
2
|
+
|
|
3
|
+
Framework-agnostic TypeScript SDK for the [Langsys](https://Langsys.dev/) Translation Manager. Realtime, continuous translations with automatic token discovery — no framework required.
|
|
4
|
+
|
|
5
|
+
This is the base SDK. Framework-specific bindings (`langsys-js-svelte`, and similar packages for React / Vue / Solid that can be built on top of this) are thin wrappers that add idiomatic framework reactivity around the same core.
|
|
6
|
+
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+
## What's inside
|
|
10
|
+
|
|
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.
|
|
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
|
+
- `Translate` class — wraps a DOM element, walks text + translatable attributes, registers a content block with the Translation Manager, re-translates on locale change.
|
|
15
|
+
- `Signal<T>` — tiny subscribe/set/update/get primitive. Compatible with Svelte's store contract by design, so the Svelte binding is nearly trivial.
|
|
16
|
+
- `LangsysAppAPI` — direct HTTP access if you want to bypass everything else.
|
|
17
|
+
- Zero runtime dependencies. Works in browsers, Node, any bundler.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install langsys-js-typescript
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { LangsysApp, createSignal, t } from 'langsys-js-typescript';
|
|
29
|
+
|
|
30
|
+
// 1. A reactive holder for the user's current locale. Use `createSignal` if you
|
|
31
|
+
// have nothing of your own — any object with subscribe/set/update/get works.
|
|
32
|
+
const userLocale = createSignal('en-us');
|
|
33
|
+
|
|
34
|
+
// 2. Initialize
|
|
35
|
+
const response = await LangsysApp.init({
|
|
36
|
+
projectid: process.env.LANGSYS_PROJECT_ID!,
|
|
37
|
+
key: process.env.LANGSYS_API_KEY!,
|
|
38
|
+
UserLocaleStore: userLocale,
|
|
39
|
+
baseLocale: 'en-us',
|
|
40
|
+
debug: false,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!response.status) console.error('Langsys init failed', response.errors);
|
|
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');
|
|
48
|
+
|
|
49
|
+
// 4. Interpolate
|
|
50
|
+
document.querySelector('#greeting')!.textContent =
|
|
51
|
+
t('Greetings', 'Hello, {name}!', { name: 'Sarah' });
|
|
52
|
+
|
|
53
|
+
// 5. Change locale — all subscribed consumers re-translate
|
|
54
|
+
userLocale.set('es-es');
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### How `t()` works
|
|
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.
|
|
60
|
+
|
|
61
|
+
#### Compile-time-checked interpolation
|
|
62
|
+
|
|
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
|
+
|
|
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'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Allowed value types: `string | number | Date | boolean`. Dates serialize to ISO 8601.
|
|
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 })`.
|
|
74
|
+
|
|
75
|
+
#### Categorization disambiguates context
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
t('Main Menu', 'Home'); // 'Inicio' in Spanish
|
|
79
|
+
t('Home repairs', 'Home'); // 'Hogar' in Spanish
|
|
80
|
+
```
|
|
81
|
+
|
|
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.
|
|
83
|
+
|
|
84
|
+
### API key permissions
|
|
85
|
+
|
|
86
|
+
- **Write key** (development) — new phrases and content blocks discovered as your app runs are registered automatically.
|
|
87
|
+
- **Read-only key** (production) — only fetches existing translations; no token creation, no content-block writes.
|
|
88
|
+
|
|
89
|
+
Detected automatically from the validation response — you don't configure this manually.
|
|
90
|
+
|
|
91
|
+
## Reactivity for frameworks
|
|
92
|
+
|
|
93
|
+
The base SDK is intentionally framework-agnostic, but it exposes the right primitives for any framework to bind to.
|
|
94
|
+
|
|
95
|
+
### Pattern: `tSignal` is the source of reactivity
|
|
96
|
+
|
|
97
|
+
`tSignal` is a `Signal<TFunction>` that re-emits a fresh closure every time translations or the loaded locale change. Subscribe and you'll be notified to re-render; read `.get()` (or call the function the signal currently holds) for the latest value.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { tSignal } from 'langsys-js-typescript';
|
|
101
|
+
|
|
102
|
+
const unsub = tSignal.subscribe((t) => {
|
|
103
|
+
// t is the current translation function — call it for the latest translations
|
|
104
|
+
document.querySelector('h1')!.textContent = t('Home', 'Welcome');
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### React
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import { useSyncExternalStore } from 'react';
|
|
112
|
+
import { tSignal, type TFunction } from 'langsys-js-typescript';
|
|
113
|
+
|
|
114
|
+
export function useT(): TFunction {
|
|
115
|
+
return useSyncExternalStore(
|
|
116
|
+
(notify) => tSignal.subscribe(notify),
|
|
117
|
+
() => tSignal.get(),
|
|
118
|
+
() => tSignal.get(),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// In a component:
|
|
123
|
+
function Header() {
|
|
124
|
+
const t = useT();
|
|
125
|
+
return <h1>{t('Home', 'Welcome, {name}!', { name: 'Sarah' })}</h1>;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
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.
|
|
130
|
+
|
|
131
|
+
### Svelte
|
|
132
|
+
|
|
133
|
+
Use the dedicated [`langsys-js-svelte`](https://github.com/langsys/langsys-js-svelte) wrapper — `Signal<T>` already satisfies Svelte's `Readable<T>` contract structurally, so the wrapper is mostly type-relabeling and a small `Writable<string>` → `Signal<string>` adapter for the user-locale store.
|
|
134
|
+
|
|
135
|
+
### Vue / Solid / anything else
|
|
136
|
+
|
|
137
|
+
The same pattern: subscribe to `tSignal` for invalidation, call the current `TFunction` for values. ~5-10 lines of binding code per framework.
|
|
138
|
+
|
|
139
|
+
## The `Translate` class
|
|
140
|
+
|
|
141
|
+
For larger blocks of HTML — articles, help text, multi-sentence markup — use `Translate` to wrap an existing DOM element. It tokenizes text nodes and translatable attributes, registers the block with the Translation Manager (so translators see your styled markup), and re-translates on locale change.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { Translate } from 'langsys-js-typescript';
|
|
145
|
+
|
|
146
|
+
const article = document.querySelector<HTMLElement>('#article')!;
|
|
147
|
+
const handle = new Translate(article, { category: 'Blog', label: 'Welcome post' });
|
|
148
|
+
|
|
149
|
+
// Locale changes are picked up automatically. Call .destroy() to stop.
|
|
150
|
+
handle.destroy();
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Attributes honored on contained elements:
|
|
154
|
+
|
|
155
|
+
- `placeholder`, `alt`, `title`, `aria-label`, `aria-placeholder`
|
|
156
|
+
- `value` on `<button>`, `<input type="submit">`, `<input type="button">`
|
|
157
|
+
- `<option>` text inside `<select>`
|
|
158
|
+
- Several validation-message `data-*` attributes
|
|
159
|
+
- `translate="no"` — elements marked this way (and their children) are skipped
|
|
160
|
+
|
|
161
|
+
## Server-Side Rendering
|
|
162
|
+
|
|
163
|
+
Pre-fetch translations on the server and seed them through `initialTranslations` to skip the duplicate client fetch on hydration:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
await LangsysApp.init({
|
|
167
|
+
projectid: env.LANGSYS_PROJECT_ID,
|
|
168
|
+
key: env.LANGSYS_API_KEY,
|
|
169
|
+
UserLocaleStore: userLocale,
|
|
170
|
+
baseLocale: 'en-us',
|
|
171
|
+
initialTranslations, // fetched on the server
|
|
172
|
+
initialTranslationsLocale: 'es-es',
|
|
173
|
+
ssrTokenStrategy: 'client', // 'client' | 'server' | 'auto'
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
- `'client'` (default) — tokens discovered during SSR flush from the client after hydration.
|
|
178
|
+
- `'server'` — flush tokens immediately from the server.
|
|
179
|
+
- `'auto'` — small batches (≤5) flush from server, larger batches wait for the client.
|
|
180
|
+
|
|
181
|
+
## Detecting the user's preferred locale
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
// Browser — uses navigator.languages with fallback to navigator.language
|
|
185
|
+
const locale = LangsysApp.detectPreferredLocale();
|
|
186
|
+
|
|
187
|
+
// SSR — parse Accept-Language header
|
|
188
|
+
const locale = LangsysApp.detectPreferredLocale(req.headers['accept-language']);
|
|
189
|
+
|
|
190
|
+
// Match against your app's supported locales, falling back to language-only matches
|
|
191
|
+
const supported = (await LangsysApp.getLocalesFlat()).map((l) => l.code);
|
|
192
|
+
const locale = LangsysApp.detectPreferredLocale(
|
|
193
|
+
req.headers['accept-language'],
|
|
194
|
+
supported,
|
|
195
|
+
);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The matcher tries exact match first (`en-US`), then language-only (`en` matches `en-GB`), then returns `null`.
|
|
199
|
+
|
|
200
|
+
## Localized country / currency / locale lists
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const countries = await LangsysApp.getCountries(); // [{ code, label }, ...]
|
|
204
|
+
const dialCodes = await LangsysApp.getDialCodes(); // [{ country_code, dial_code, name }, ...]
|
|
205
|
+
const currencies = await LangsysApp.getCurrencies(); // [{ code, name, symbol, ... }, ...]
|
|
206
|
+
const locales = await LangsysApp.getLocales(); // { LanguageName: [{ code, name }] }
|
|
207
|
+
const localeName = await LangsysApp.getLocaleNameWithLookup('es-es', true, 'fr-fr'); // 'espagnol'
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## The `Signal<T>` primitive
|
|
211
|
+
|
|
212
|
+
A tiny observable value holder. Used internally by `tSignal` and by user-supplied locale stores. You can use it for your own state too, though that's not its primary purpose.
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { createSignal, getValue } from 'langsys-js-typescript';
|
|
216
|
+
|
|
217
|
+
const count = createSignal(0);
|
|
218
|
+
const unsub = count.subscribe((v) => console.log('count:', v));
|
|
219
|
+
count.set(1);
|
|
220
|
+
count.update((n) => n + 1);
|
|
221
|
+
getValue(count); // 2
|
|
222
|
+
unsub();
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Why it matters for framework bindings:** the `Signal<T>` contract is `subscribe(run): Unsubscriber; set(v); update(fn); get(): T` with subscribe-fires-immediately semantics. Svelte's store contract is the same minus `.get()`. So:
|
|
226
|
+
|
|
227
|
+
- Anything that `Signal<T>` produces is a valid Svelte `Readable<T>`.
|
|
228
|
+
- Svelte writables can be adapted to `Signal<T>` with a 5-line `.get()` polyfill.
|
|
229
|
+
- React/Vue/Solid integrations subscribe to it the same way they'd subscribe to any external store.
|
|
230
|
+
|
|
231
|
+
## Full API
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import type {
|
|
235
|
+
iLangsysInitConfig,
|
|
236
|
+
iLangsysResponse,
|
|
237
|
+
iCategories,
|
|
238
|
+
iContentBlock,
|
|
239
|
+
iCountryList,
|
|
240
|
+
iCountryDialCode,
|
|
241
|
+
iCurrencyList,
|
|
242
|
+
iLocaleDefault,
|
|
243
|
+
iLocaleFlat,
|
|
244
|
+
iLocaleData,
|
|
245
|
+
TFunction,
|
|
246
|
+
TranslationParams,
|
|
247
|
+
ParamPrimitive,
|
|
248
|
+
ExtractParamKeys,
|
|
249
|
+
ParamsFor,
|
|
250
|
+
Signal,
|
|
251
|
+
} from 'langsys-js-typescript';
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
- `LangsysApp.init(config)` — initialize, returns an `iLangsysResponse`.
|
|
255
|
+
- `LangsysApp.refresh()` — force-refetch translations for the current locale.
|
|
256
|
+
- `LangsysApp.t` — current `TFunction` (getter; reads fresh state on every call).
|
|
257
|
+
- `LangsysApp.translationsLoadingPromise` — resolves when the current locale's translations are ready.
|
|
258
|
+
- `LangsysApp.detectPreferredLocale(header?, supported?)` — locale detection.
|
|
259
|
+
- `LangsysApp.getCountries(inLocale?)` / `.getCountryName(code, inLocale?)`
|
|
260
|
+
- `LangsysApp.getDialCodes(inLocale?)`
|
|
261
|
+
- `LangsysApp.getCurrencies(inLocale?)` / `.getCurrencyName(code, inLocale?)`
|
|
262
|
+
- `LangsysApp.getLocales(inLocale?)` / `.getLocalesFlat(inLocale?)` / `.getLocalesData(inLocale?, force?)`
|
|
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`.
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT
|