@stream-io/i18n
v1.0.2
Published
Translation runtime shared across Stream.io JS SDKs: i18next wrapper, date/time formatters and a build-time catalog generator
Downloads
1,134
Readme
@stream-io/i18n
The translation runtime shared across Stream's JS SDKs — an i18next
wrapper, a date/time layer built on dayjs, and a build-time generator that
derives a type-only translation catalog from your t() call sites.
Framework-agnostic: nothing in the main entry point knows about Chat, Video or Feeds.
import { Streami18n } from "@stream-io/i18n";
const i18n = new Streami18n({ language: "de" });
i18n.registerTranslation("de", { "common.cancel.label": "Abbrechen" });
await i18n.init();
i18n.t("common.cancel.label"); // "Abbrechen"Entry points
| Import | Contents |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| @stream-io/i18n | Streami18n, formatters, getDateString, the dayjs layer, TranslationStore, TranslationBuilder, catalog-generic type helpers |
| @stream-io/i18n/react | useStreami18nState, createTranslationContext |
| @stream-io/i18n/codegen | The catalog generator. Node-only, ESM-only, build-time |
react is the only peer dependency, and it is optional — needed just for the /react subpath.
i18next, dayjs, @stream-io/state-store and use-sync-external-store are direct dependencies,
so nothing extra has to be installed alongside. use-sync-external-store in particular is a
dependency rather than a peer because /react imports it unconditionally, to keep the React 17
floor.
No catalog ships with this package
Each SDK generates its own keys.ts from its own t() call sites, so the type helpers are
generic over it:
import type { StreamTFunctionFor } from "@stream-io/i18n";
import type { TranslationCatalog, BundledKey } from "./i18n/keys.ts";
type StreamTFunction = StreamTFunctionFor<TranslationCatalog, BundledKey>;Bundled must default to never. Defaulting it to string collapses the prose overload and
silently disables all key checking.
Keys with no possible inline default — formatter expressions such as timestamp.* — are supplied
through the runtimeDefaults constructor option rather than imported, because the catalog belongs
to the UI layer. They are layered under every language, which is what stops a partial dictionary
from knocking out formatter keys.
Reactivity
Streami18n.state is a StateStore, so language changes propagate without
listeners:
const unsubscribe = i18n.state.subscribe(({ t, language }) => render(t, language));subscribe fires synchronously with the current value. In React, use the binding:
import { useStreami18nState } from "@stream-io/i18n/react";
const { t, tDateTimeParser } = useStreami18nState(i18n);init() is memoized, so several components sharing one instance share one initialization.
Guarantees
Three behaviours are covered by dedicated tests, each written against a real bug
(src/__tests__/Streami18nGuarantees.test.ts):
- G1 — the SDK's bundled defaults are layered under every language, however it was selected.
- G2 — a partial dictionary is safe: an unsupplied key renders English, never a raw dotted path.
- G3 — selecting an unregistered language warns and continues; it must not silently reset to
en.
Notes
- No module-scope side effects. Every
Dayjs.extendgoes throughensureDayjsPlugins(), which is what makessideEffects: falseaccurate. Do not reintroduce a top-levelextend. durationFormattertakes milliseconds, not a timestamp. Parsing600000as a timestamp renders "57 years ago".- i18next post-processing is global. A
TranslationTopicis invoked for every key and must pass through calls it does not recognize, or it silently rewrites unrelated copy. Intl.PluralRulescoverage is warned about, not polyfilled. Hermes ships a partial ICU; React Native consumers should importintl-pluralrulesthemselves beforeinit().- Date assertions are timezone-sensitive — the test script pins
TZ=UTC.
