Framework-agnostic TypeScript SDK for the Langsys Translation Manager. Realtime, continuous translations with automatic token discovery — no framework required.
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.
LangsysAppsingleton — init, locale switching, localized country/currency/locale helpers, preferred-locale detection.t(category, phrase, params?)— the everyday translation function. The phrase is both the lookup key and the base-language default. Curly-brace placeholders interpolate fromparamsand are compile-time type-checked.tSignal: Signal<TFunction>— reactive primitive that re-emits a freshton every translations/locale change. Framework bindings subscribe to this to drive re-renders.Translateclass — wraps a DOM element, walks text + translatable attributes, registers a content block with the Translation Manager, re-translates on locale change.Signal<T>— tiny subscribe/set/update/get primitive. Compatible with Svelte's store contract by design, so the Svelte binding is nearly trivial.LangsysAppAPI— direct HTTP access if you want to bypass everything else.- Zero runtime dependencies. Works in browsers, Node, any bundler.
npm install langsys-js-typescriptimport { LangsysApp, createSignal, t } from 'langsys-js-typescript';
// 1. A reactive holder for the user's current locale. Use `createSignal` if you
// have nothing of your own — any object with subscribe/set/update/get works.
const userLocale = createSignal('en-us');
// 2. Initialize
const response = await LangsysApp.init({
projectid: process.env.LANGSYS_PROJECT_ID!,
key: process.env.LANGSYS_API_KEY!,
UserLocaleStore: userLocale,
baseLocale: 'en-us',
debug: false,
});
if (!response.status) console.error('Langsys init failed', response.errors);
// 3. Translate
document.querySelector('h1')!.textContent = t('Home', 'Welcome to my app');
document.querySelector('#tagline')!.textContent = t('Marketing', 'We translate everything');
// 4. Interpolate
document.querySelector('#greeting')!.textContent =
t('Greetings', 'Hello, {name}!', { name: 'Sarah' });
// 5. Change locale — all subscribed consumers re-translate
userLocale.set('es-es');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.
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:
t('Greetings', 'Hello, {name}!', { name: 'Sarah' }); // OK
t('Greetings', 'Hello, {name}!', {}); // ❌ Property 'name' missing
t('Greetings', 'Hello, {name}!', { name: 'x', extra: 'y' }); // ❌ Unknown property 'extra'Allowed value types: string | number | Date | boolean. Dates serialize to ISO 8601.
Future versions will swap the simple
{name}runtime for ICU MessageFormat — adding plural / select / date formatting — without changing the public signature. Today'st('Cart', '{count} items', { count })will evolve tot('Cart', '{count, plural, one {# item} other {# items}}', { count }).
t('Main Menu', 'Home'); // 'Inicio' in Spanish
t('Home repairs', 'Home'); // 'Hogar' in SpanishThe 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.
- Write key (development) — new phrases and content blocks discovered as your app runs are registered automatically.
- Read-only key (production) — only fetches existing translations; no token creation, no content-block writes.
Detected automatically from the validation response — you don't configure this manually.
The base SDK is intentionally framework-agnostic, but it exposes the right primitives for any framework to bind to.
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.
import { tSignal } from 'langsys-js-typescript';
const unsub = tSignal.subscribe((t) => {
// t is the current translation function — call it for the latest translations
document.querySelector('h1')!.textContent = t('Home', 'Welcome');
});import { useSyncExternalStore } from 'react';
import { tSignal, type TFunction } from 'langsys-js-typescript';
export function useT(): TFunction {
return useSyncExternalStore(
(notify) => tSignal.subscribe(notify),
() => tSignal.get(),
() => tSignal.get(),
);
}
// In a component:
function Header() {
const t = useT();
return <h1>{t('Home', 'Welcome, {name}!', { name: 'Sarah' })}</h1>;
}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.
Use the dedicated 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.
The same pattern: subscribe to tSignal for invalidation, call the current TFunction for values. ~5-10 lines of binding code per framework.
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.
import { Translate } from 'langsys-js-typescript';
const article = document.querySelector<HTMLElement>('#article')!;
const handle = new Translate(article, { category: 'Blog', label: 'Welcome post' });
// Locale changes are picked up automatically. Call .destroy() to stop.
handle.destroy();Attributes honored on contained elements:
placeholder,alt,title,aria-label,aria-placeholdervalueon<button>,<input type="submit">,<input type="button"><option>text inside<select>- Several validation-message
data-*attributes translate="no"— elements marked this way (and their children) are skipped
Pre-fetch translations on the server and seed them through initialTranslations to skip the duplicate client fetch on hydration:
await LangsysApp.init({
projectid: env.LANGSYS_PROJECT_ID,
key: env.LANGSYS_API_KEY,
UserLocaleStore: userLocale,
baseLocale: 'en-us',
initialTranslations, // fetched on the server
initialTranslationsLocale: 'es-es',
ssrTokenStrategy: 'client', // 'client' | 'server' | 'auto'
});'client'(default) — tokens discovered during SSR flush from the client after hydration.'server'— flush tokens immediately from the server.'auto'— small batches (≤5) flush from server, larger batches wait for the client.
// Browser — uses navigator.languages with fallback to navigator.language
const locale = LangsysApp.detectPreferredLocale();
// SSR — parse Accept-Language header
const locale = LangsysApp.detectPreferredLocale(req.headers['accept-language']);
// Match against your app's supported locales, falling back to language-only matches
const supported = (await LangsysApp.getLocalesFlat()).map((l) => l.code);
const locale = LangsysApp.detectPreferredLocale(
req.headers['accept-language'],
supported,
);The matcher tries exact match first (en-US), then language-only (en matches en-GB), then returns null.
const countries = await LangsysApp.getCountries(); // [{ code, label }, ...]
const dialCodes = await LangsysApp.getDialCodes(); // [{ country_code, dial_code, name }, ...]
const currencies = await LangsysApp.getCurrencies(); // [{ code, name, symbol, ... }, ...]
const locales = await LangsysApp.getLocales(); // { LanguageName: [{ code, name }] }
const localeName = await LangsysApp.getLocaleNameWithLookup('es-es', true, 'fr-fr'); // 'espagnol'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.
import { createSignal, getValue } from 'langsys-js-typescript';
const count = createSignal(0);
const unsub = count.subscribe((v) => console.log('count:', v));
count.set(1);
count.update((n) => n + 1);
getValue(count); // 2
unsub();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:
- Anything that
Signal<T>produces is a valid SvelteReadable<T>. - Svelte writables can be adapted to
Signal<T>with a 5-line.get()polyfill. - React/Vue/Solid integrations subscribe to it the same way they'd subscribe to any external store.
import type {
iLangsysInitConfig,
iLangsysResponse,
iCategories,
iContentBlock,
iCountryList,
iCountryDialCode,
iCurrencyList,
iLocaleDefault,
iLocaleFlat,
iLocaleData,
TFunction,
TranslationParams,
ParamPrimitive,
ExtractParamKeys,
ParamsFor,
Signal,
} from 'langsys-js-typescript';LangsysApp.init(config)— initialize, returns aniLangsysResponse.LangsysApp.refresh()— force-refetch translations for the current locale.LangsysApp.t— currentTFunction(getter; reads fresh state on every call).LangsysApp.translationsLoadingPromise— resolves when the current locale's translations are ready.LangsysApp.detectPreferredLocale(header?, supported?)— locale detection.LangsysApp.getCountries(inLocale?)/.getCountryName(code, inLocale?)LangsysApp.getDialCodes(inLocale?)LangsysApp.getCurrencies(inLocale?)/.getCurrencyName(code, inLocale?)LangsysApp.getLocales(inLocale?)/.getLocalesFlat(inLocale?)/.getLocalesData(inLocale?, force?)LangsysApp.getLocaleName(code, short?, inLocale?)/.getLocaleNameWithLookup(...)- Top-level:
t,tSignal,currentlyLoadedLocale,sTranslations,contentBlocks,LangsysAppAPI,Translate,createSignal,getValue,persist,interpolate,Logger,logger,md5,isEmpty.
MIT