From eba43761e3a52514fe5789c407ae5aad0b84aa81 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Thu, 10 Sep 2026 15:03:05 +0200 Subject: [PATCH 1/6] refactor(i18n)!: consume the shared runtime from @stream-io/i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: requires `@stream-io/i18n` and `stream-chat@^10.0.0-rc.10`. Retargets every `stream-chat/i18n` import to `@stream-io/i18n`, which is where the shared runtime now lives. The API is unchanged; only the specifier moves. `languageNameDefaults` / `LanguageNameCatalog` come from `stream-chat`'s root barrel instead — they enumerate the languages the Chat API can auto-translate a message into, so they stayed with Chat rather than moving to the generic package. `TranslationContext` and `useStreami18n` are now built on the shared bindings in `@stream-io/i18n/react`, so this SDK and the React Native one no longer carry two copies of the same context plumbing and store subscription. What stays here is what is genuinely this SDK's: the catalog-typed `t`, the notification translation topic, and supplying a context *default* rather than throwing, which is what lets a primitive render outside ``. `@stream-io/i18n` is a regular dependency, not a peer: an integrator never imports it — they import the `Streami18n` this package subclasses and re-exports. Also fixes `yarn types`, which silently checked nothing. It ran `tsc` with no `--project`, picking up the root solution file (`"files": []`), and exited 0 even with a deliberate error in `src/`. It is now `tsc -p tsconfig.lib.json --noEmit` and is wired into CI, which previously typechecked only the build scripts. --- .github/workflows/ci.yml | 5 +- .github/workflows/size.yml | 3 -- AGENTS.md | 2 +- CLAUDE.md | 44 +++++++++++++---- ai-docs/i18n-v15-migration.md | 2 +- package.json | 9 ++-- scripts/generate-i18n-keys.mts | 6 +-- src/context/TranslationContext.tsx | 48 +++++++++---------- src/i18n/Streami18n.ts | 7 +-- src/i18n/TranslationBuilder/index.ts | 4 +- .../NotificationTranslationBuilder.test.ts | 2 +- src/i18n/types.ts | 10 ++-- src/i18n/useStreami18n.ts | 31 +++++------- src/i18n/utils.ts | 6 +-- 14 files changed, 99 insertions(+), 80 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2e10bece..e26a770ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ permissions: jobs: lint: runs-on: ubuntu-latest - name: ESLint & Prettier + name: ESLint, Prettier & Types steps: - uses: actions/checkout@v6 @@ -28,6 +28,9 @@ jobs: - run: yarn lint + - name: Typecheck the library + run: yarn types + # scripts/ is outside the src tsconfigs and is not linted; Node 24 strips the types in # the .mts build scripts without checking them, so check them here. - name: Typecheck build scripts diff --git a/.github/workflows/size.yml b/.github/workflows/size.yml index 80482282b..34b41ca69 100644 --- a/.github/workflows/size.yml +++ b/.github/workflows/size.yml @@ -4,9 +4,6 @@ on: pull_request: branches: - master - # The v15 release branch. Without it this workflow does not run on any PR stacked onto it, so the - # i18n consolidation's central size claim -- that moving the runtime into `stream-chat/i18n` - # shrinks the root bundle -- goes unmeasured for the whole release. - release-v15 paths-ignore: - '**.test.*' diff --git a/AGENTS.md b/AGENTS.md index ada19dcaa..7ead8230f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,7 +50,7 @@ Respect any repo-specific rules. Do not suppress rules broadly; justify and scop 1. Install dependencies (root + all workspaces): yarn install 2. Build: yarn build -3. Typecheck: yarn types +3. Typecheck: yarn types (the library, via tsconfig.lib.json) 4. Lint: yarn lint 5. Fix lint issues: yarn lint-fix 6. Unit tests: yarn test diff --git a/CLAUDE.md b/CLAUDE.md index 316ec32b0..c42bd838c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,8 +25,8 @@ yarn test # Run specific test (e.g., yarn test Channel) yarn lint-fix # Fix all lint/format issues (prettier + eslint) # Type checking -tsc -p tsconfig.lib.json --noEmit # The library. THIS is the real check. -yarn types:scripts # scripts/*.mts (Node strips types, it does not check them) +yarn types # Typecheck the library (tsc -p tsconfig.lib.json --noEmit) +yarn types:scripts # scripts/*.mts (Node strips types, it does not check them) # i18n (see the i18n System section) yarn build-translations # Regenerate src/i18n/keys.ts from the t() call sites @@ -41,9 +41,14 @@ yarn examples:build # Build all examples yarn lint-fix # ALWAYS run this first ``` -> **`yarn types` checks nothing — do not rely on it.** It runs `tsc` with no `--project`, so it -> picks up the root `tsconfig.json`, which is a solution file with `"files": []`. It exits 0 even -> with a deliberate type error in `src/`. Use `tsc -p tsconfig.lib.json --noEmit`. +> **`yarn types` is `tsc -p tsconfig.lib.json --noEmit`** — it typechecks the library and nothing +> else. It used to run `tsc` with no `--project`, which picked up the root `tsconfig.json` (a +> solution file with `"files": []`) and exited 0 even with a deliberate type error in `src/`; that +> is fixed. `tsconfig.lib.json` excludes `__tests__`, `stories` and `mock-builders`, so those are +> not covered here. +> +> **CI runs it** in the `ESLint, Prettier & Types` job, alongside `yarn lint` and +> `yarn types:scripts` — the cheapest job to fail in, since it needs no build. > > `yarn types:tests` (`tsconfig.test.json`) reports ~1200 pre-existing errors and is not wired into > CI. Treat it as unenforced. @@ -283,7 +288,7 @@ Closes #123 - [ ] `yarn lint-fix` passed - [ ] `yarn test` passed -- [ ] `tsc -p tsconfig.lib.json --noEmit` passed (NOT `yarn types` — see Essential Commands) +- [ ] `yarn types` passed (typechecks the library) - [ ] `yarn validate-translations` passed, if any `t()` call changed - [ ] Tests added for changes - [ ] No new warnings (zero tolerance) @@ -326,6 +331,26 @@ See `examples/vite/src/index.scss` for reference implementation. Layers eliminat ## i18n System +**The runtime is `@stream-io/i18n`**, shared with the React Native SDK — one `Streami18n`, one set +of formatters, one date layer. This package owns only what is genuinely its own: the generated key +catalog, `runtimeDefaults.ts`, the React context/hook binding, and the notification translation +topic. `src/i18n/Streami18n.ts` is a thin subclass injecting this SDK's bundled data. + +It is a **regular dependency**, not a peer: an integrator never imports `@stream-io/i18n` — they +import `Streami18n` from this package, which subclasses it. Peer dependencies are for things the +app itself constructs and hands back (`react`, `stream-chat`), which is not the case here. + +Two resolved copies of `i18next` would be harmless anyway — `Streami18n` calls +`i18next.createInstance()` and registers dictionaries on that instance, never on a global. `dayjs` +is the one that genuinely wants a single copy, because locale registration _is_ global: an app +adding a language does `import 'dayjs/locale/de.js'`, and that has to land in the same registry the +SDK reads. Keeping the range compatible (`^1.11.13`) is what makes them dedupe. Do not add +`i18next` as a direct dependency here. + +The `language.*` names (`languageNameDefaults`, `LanguageNameCatalog`) still come from +`stream-chat`, not from `@stream-io/i18n` — they enumerate the languages the Chat API can +auto-translate a message into, so they are Chat API metadata. + **English only.** Every other language is supplied by the integrator via `Streami18n.registerTranslation()`. @@ -365,8 +390,11 @@ renders English — and it keeps the copy visible at the call site. - **Runtime keys:** the ~10 keys resolved from a runtime value (a `stream-chat` `notification.message`, slash-command metadata, a language code, an integrator prop) go through `asDynamicKey()`. That brand is required, so every escape is deliberate and greppable. - `src/i18n/externalStrings.ts` maps the `stream-chat` messages we recognise onto stable keys. -- **`yarn build-translations`** parses the `t()` call sites (`scripts/i18n-call-sites.mts`), joins + A `stream-chat` notification is resolved by its stable `type` identifier + (`CORE_NOTIFICATION_TYPE`) through `src/i18n/TranslationBuilder/notifications/`, not by matching + its English prose — the `externalStrings.ts` table that used to do the latter is gone. +- **`yarn build-translations`** parses the `t()` call sites (via `@stream-io/i18n/codegen`, driven by + `scripts/generate-i18n-keys.mts`), joins them with `runtimeDefaults.ts`, and regenerates `keys.ts`. It hard-fails on three things: a key used with two different inline copies; a key called with no inline default and no `runtimeDefaults` entry (it would render as the raw dotted key); and a key present in _both_ diff --git a/ai-docs/i18n-v15-migration.md b/ai-docs/i18n-v15-migration.md index 58c09f518..f2cb8fa6b 100644 --- a/ai-docs/i18n-v15-migration.md +++ b/ai-docs/i18n-v15-migration.md @@ -320,7 +320,7 @@ with no dictionary behind it. Also now internal, none of them documented before: `translations`, `dayjsLocales`, `isCustomDateTimeParser`, `localeExists()`, `addOrUpdateLocale()`, `validateCurrentLanguage()`. To -register a dayjs locale directly, `stream-chat/i18n` exports `addOrUpdateDayjsLocale()`. +register a dayjs locale directly, `@stream-io/i18n` exports `addOrUpdateDayjsLocale()`. ### `useChat` no longer returns `translators` diff --git a/package.json b/package.json index 1bd41ed56..e375a6f33 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "@braintree/sanitize-url": "^7.1.2", "@floating-ui/react": "^0.27.19", "@react-aria/focus": "^3.22.0", + "@stream-io/i18n": "^1.0.0", "clsx": "^2.1.1", "emoji-regex": "^9.2.2", "fix-webm-duration": "^1.0.6", @@ -116,7 +117,7 @@ "unified": "^11.0.5", "unist-builder": "^4.0.0", "unist-util-visit": "^5.1.0", - "use-sync-external-store": "^1.6.0" + "use-sync-external-store": "^1.7.0" }, "optionalDependencies": { "@stream-io/transliterate": "^1.5.5" @@ -129,7 +130,7 @@ "modern-normalize": "^3.0.1", "react": "^19.0.0 || ^18.0.0 || ^17.0.0", "react-dom": "^19.0.0 || ^18.0.0 || ^17.0.0", - "stream-chat": "10.0.0-rc.9" + "stream-chat": "^10.0.0-rc.10" }, "peerDependenciesMeta": { "@breezystack/lamejs": { @@ -199,7 +200,7 @@ "react-dom": "^19.2.6", "sass": "^1.100.0", "semantic-release": "^25.0.3", - "stream-chat": "10.0.0-rc.9", + "stream-chat": "^10.0.0-rc.10", "typescript": "^6.0.3", "typescript-eslint": "^8.59.4", "vite": "^8.1.3", @@ -227,7 +228,7 @@ "postinstall": "node -e \"require('fs').existsSync('scripts/install-husky.mjs') && import('./scripts/install-husky.mjs')\"", "test": "vitest run", "test:watch": "vitest", - "types": "tsc --emitDeclarationOnly false --noEmit", + "types": "tsc -p tsconfig.lib.json --noEmit", "types:tests": "tsc --project tsconfig.test.json --noEmit", "validate-translations": "yarn build-translations && git diff --exit-code -- src/i18n/keys.ts", "validate-cjs": "concurrently 'node scripts/validate-cjs-node-bundle.cjs' 'node scripts/validate-cjs-browser-bundle.cjs'", diff --git a/scripts/generate-i18n-keys.mts b/scripts/generate-i18n-keys.mts index d3564e4ec..341fac7a5 100644 --- a/scripts/generate-i18n-keys.mts +++ b/scripts/generate-i18n-keys.mts @@ -2,14 +2,14 @@ // copy. The i18n types derive `TranslationKey` / `StreamTFunction` from it, so a typo'd key is a compile // error rather than a string that silently stops rendering. // -// The generator itself lives in `stream-chat/i18n/codegen`, shared with the React Native SDK. Only this +// The generator itself lives in `@stream-io/i18n/codegen`, shared with the React Native SDK. Only this // package's paths and prefixes are configured here; the call-site reader, the four hard-fail guards and // the emitter are all core's. // // Run by `yarn build-translations`, from the package root — every path below is relative to it. // `yarn validate-translations` runs it and fails on any diff, which is the drift gate. import ts from 'typescript'; -import { generateI18nKeys } from 'stream-chat/i18n/codegen'; +import { generateI18nKeys } from '@stream-io/i18n/codegen'; const jsonFlag = process.argv.indexOf('--json'); const jsonOut = jsonFlag === -1 ? undefined : process.argv[jsonFlag + 1]; @@ -22,7 +22,7 @@ if (jsonFlag !== -1 && (!jsonOut || jsonOut.startsWith('--'))) { try { generateI18nKeys({ fixtureOut: 'src/i18n/__tests__/catalog.fixture.json', - // `language.*` names come from `stream-chat/i18n` rather than from this package's + // `language.*` names come from `@stream-io/i18n` rather than from this package's // runtimeDefaults, so they are excluded from the translator export alongside the formatter // expressions — a TMS should not be asked to translate the SDK's own language list. extraFormatterPrefixes: ['translationBuilderTopic.', 'language.'], diff --git a/src/context/TranslationContext.tsx b/src/context/TranslationContext.tsx index 4e19501c6..c4d964018 100644 --- a/src/context/TranslationContext.tsx +++ b/src/context/TranslationContext.tsx @@ -1,19 +1,15 @@ -import type { PropsWithChildren } from 'react'; -import React, { useContext } from 'react'; +import { createTranslationContext } from '@stream-io/i18n/react'; import { defaultDateTimeParser, defaultTranslatorFunction } from '../i18n/utils'; import type { StreamTFunction, TDateTimeParser } from '../i18n/types'; /** - * The `Dayjs.extend(calendar)` / `extend(localizedFormat)` calls that used to sit here are gone. + * The `Dayjs.extend(calendar)` / `extend(localizedFormat)` calls that used to sit here are gone: + * `defaultDateTimeParser` from `@stream-io/i18n` registers the plugins itself on first use, which + * is what lets the package stay side-effect-free. * - * They existed so that the context *default* — used by a component rendered outside `` — could - * still call `.calendar()`. `defaultDateTimeParser` now comes from `stream-chat/i18n` and registers the - * plugins itself on first use, so the same guarantee holds without a module-scope side effect. That is - * what lets the package be marked side-effect-free. - * - * Worth knowing if this ever regresses: extending dayjs is not optional here, and forgetting it fails - * *silently* — `.calendar()` is simply absent, so timestamps render malformed rather than throwing. + * If that ever regresses it fails *silently* — `.calendar()` is simply absent, so timestamps render + * malformed rather than throwing. */ export type TranslationContextValue = { @@ -22,21 +18,21 @@ export type TranslationContextValue = { userLanguage: string; }; -export const TranslationContext = React.createContext({ - t: defaultTranslatorFunction, - tDateTimeParser: defaultDateTimeParser, - userLanguage: 'en', -}); - -export const TranslationProvider = ({ - children, - value, -}: PropsWithChildren<{ value: TranslationContextValue }>) => ( - {children} -); - /** - * Works outside ``: the context default's `defaultTranslatorFunction` renders the inline - * English `defaultValue` every `t()` call site passes, so SDK primitives render standalone. + * Built from the shared factory in `@stream-io/i18n/react`. What stays this SDK's own: the + * catalog-typed `t`, and supplying a **default** rather than throwing, so primitives render outside + * `` (React Native throws instead — hence the option). */ -export const useTranslationContext = () => useContext(TranslationContext); +const { TranslationContext, TranslationProvider, useTranslationContext } = + createTranslationContext({ + defaultValue: { + t: defaultTranslatorFunction, + tDateTimeParser: defaultDateTimeParser, + userLanguage: 'en', + }, + }); + +export { TranslationContext, TranslationProvider }; + +/** Works outside `` — the default translator renders each call site's inline English. */ +export { useTranslationContext }; diff --git a/src/i18n/Streami18n.ts b/src/i18n/Streami18n.ts index 9b3c49d15..b2bafa8b9 100644 --- a/src/i18n/Streami18n.ts +++ b/src/i18n/Streami18n.ts @@ -1,5 +1,6 @@ -import { Streami18n as CoreStreami18n, languageNameDefaults } from 'stream-chat/i18n'; -import type { Streami18nOptions as CoreStreami18nOptions } from 'stream-chat/i18n'; +import { Streami18n as CoreStreami18n } from '@stream-io/i18n'; +import type { Streami18nOptions as CoreStreami18nOptions } from '@stream-io/i18n'; +import { languageNameDefaults } from 'stream-chat'; import { NotificationTranslationTopic } from './TranslationBuilder'; import { runtimeDefaults } from './runtimeDefaults'; @@ -17,7 +18,7 @@ export type Streami18nOptions = CoreStreami18nOptions; * Wrapper around [i18next](https://www.i18next.com/) for this SDK's translations. Pass an instance to * `` to control language and copy. * - * The implementation lives in `stream-chat/i18n`, shared with the React Native SDK. What is added here + * The implementation lives in `@stream-io/i18n`, shared with the React Native SDK. What is added here * is the two things that are this SDK's own: its bundled translation data, and its notification * translation topic. Core cannot import either — the key catalog is generated from *this* package's * `t()` call sites. diff --git a/src/i18n/TranslationBuilder/index.ts b/src/i18n/TranslationBuilder/index.ts index 67bcd727d..465b7690c 100644 --- a/src/i18n/TranslationBuilder/index.ts +++ b/src/i18n/TranslationBuilder/index.ts @@ -1,6 +1,6 @@ /** * The `TranslationBuilder` / `TranslationTopic` / `Translator` plumbing now lives in - * `stream-chat/i18n`, shared with the React Native SDK. Only the *topics* are this SDK's own, since + * `@stream-io/i18n`, shared with the React Native SDK. Only the *topics* are this SDK's own, since * they reference its key names. */ export { @@ -9,5 +9,5 @@ export { type TranslationTopicConstructor, type TranslationTopicOptions, type Translator, -} from 'stream-chat/i18n'; +} from '@stream-io/i18n'; export * from './notifications'; diff --git a/src/i18n/__tests__/NotificationTranslationBuilder.test.ts b/src/i18n/__tests__/NotificationTranslationBuilder.test.ts index c0c07f84e..410807fb3 100644 --- a/src/i18n/__tests__/NotificationTranslationBuilder.test.ts +++ b/src/i18n/__tests__/NotificationTranslationBuilder.test.ts @@ -1,7 +1,7 @@ import { NotificationTranslationTopic } from '../TranslationBuilder'; import { defaultNotificationTranslators } from '../TranslationBuilder/notifications/NotificationTranslationTopic'; import { fromPartial } from '@total-typescript/shoehorn'; -import type { I18nInstance } from 'stream-chat/i18n'; +import type { I18nInstance } from '@stream-io/i18n'; import type { Notification } from 'stream-chat'; const mockI18Next = fromPartial({ use: vi.fn() }); diff --git a/src/i18n/types.ts b/src/i18n/types.ts index 863c304d7..17d71af1f 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -1,17 +1,19 @@ import type { Streami18nState as CoreStreami18nState, - LanguageNameCatalog, LooseTranslationDictionaryOf, RelativeTimeCatalog, StreamTFunctionFor, TranslationDictionaryOf, TranslationKeyOf, -} from 'stream-chat/i18n'; +} from '@stream-io/i18n'; +// The names of the languages the API can auto-translate a message into. Chat API metadata, so it +// stays in `stream-chat` rather than moving to the generic i18n package. +import type { LanguageNameCatalog } from 'stream-chat'; import type { TranslationCatalog as GeneratedCatalog } from './keys'; /** - * The SDK's i18n types, instantiated from the generic helpers in `stream-chat/i18n`. + * The SDK's i18n types, instantiated from the generic helpers in `@stream-io/i18n`. * * The derivations live in core so both UI SDKs share one implementation; the *catalog* stays here, * because it is generated from this SDK's own `t()` call sites. That split is why core's helpers are @@ -65,4 +67,4 @@ export type { TDateTimeParserInput, TDateTimeParserOutput, TimestampFormatterOptions, -} from 'stream-chat/i18n'; +} from '@stream-io/i18n'; diff --git a/src/i18n/useStreami18n.ts b/src/i18n/useStreami18n.ts index 81fba9d70..4646af498 100644 --- a/src/i18n/useStreami18n.ts +++ b/src/i18n/useStreami18n.ts @@ -1,15 +1,11 @@ import { useEffect, useMemo, useState } from 'react'; +import { useStreami18nState } from '@stream-io/i18n/react'; import { Streami18n } from './Streami18n'; -import type { Streami18nState } from './types'; -import { useStateStore } from '../store'; import type { StreamChat } from 'stream-chat'; import type { TranslationContextValue } from '../context/TranslationContext'; -/** Module scope, so the subscription is not torn down and rebuilt on every render. */ -const selector = ({ t, tDateTimeParser }: Streami18nState) => ({ t, tDateTimeParser }); - export type UseStreami18nParams = { client: StreamChat; /** An instance the integrator configured. One is created when absent. */ @@ -17,15 +13,13 @@ export type UseStreami18nParams = { }; /** - * Resolves the translation context value. - * - * Two independent languages come out of here, and keeping them apart is the point: + * Resolves the translation context value. Two independent languages come out of here, and keeping + * them apart is the point: * - * - **UI copy** comes from the `Streami18n` instance, through `state`. Set it with the `language` - * option or `setLanguage()`. - * - **`userLanguage`** is the language the API auto-translates *messages* into, which is - * `client.user.language` and nothing else. Every consumer uses it to read - * `message.i18n[`${userLanguage}_text`]`. + * - **UI copy** — from the `Streami18n` instance via `useStreami18nState` (shared with the React + * Native SDK; it owns `init()` and the store subscription). + * - **`userLanguage`** — `client.user.language`, the language the API auto-translates *messages* + * into, used to read ``message.i18n[`${userLanguage}_text`]``. Chat-client state, so it stays here. */ export const useStreami18n = ({ client, @@ -33,6 +27,7 @@ export const useStreami18n = ({ }: UseStreami18nParams): TranslationContextValue => { const i18n = useMemo(() => i18nInstance ?? new Streami18n(), [i18nInstance]); const [userLanguage, setUserLanguage] = useState(() => client.user?.language ?? 'en'); + useEffect(() => { const sync = () => setUserLanguage(client.user?.language ?? 'en'); sync(); @@ -42,13 +37,9 @@ export const useStreami18n = ({ return unsubscribe; }, [client]); - useEffect(() => { - i18n.init().catch((error: unknown) => { - console.warn(`Streami18n failed to initialize`, error); - }); - }, [i18n]); - - const { t, tDateTimeParser } = useStateStore(i18n.state, selector); + const { t, tDateTimeParser } = useStreami18nState(i18n, (error) => { + console.warn(`Streami18n failed to initialize`, error); + }); return useMemo( () => ({ t, tDateTimeParser, userLanguage }), diff --git a/src/i18n/utils.ts b/src/i18n/utils.ts index df8aaf507..f840ea998 100644 --- a/src/i18n/utils.ts +++ b/src/i18n/utils.ts @@ -1,4 +1,4 @@ -import { createDefaultTranslatorFunction } from 'stream-chat/i18n'; +import { createDefaultTranslatorFunction } from '@stream-io/i18n'; import type { StreamTFunction } from './types'; @@ -13,7 +13,7 @@ export const defaultTranslatorFunction: StreamTFunction = createDefaultTranslatorFunction() as StreamTFunction; /** - * The date/time and key helpers now live in `stream-chat/i18n`, shared with the React Native SDK. + * The date/time and key helpers now live in `@stream-io/i18n`, shared with the React Native SDK. * * Re-exported from here rather than rewritten at ~15 call sites, so the internal module path stays * stable. `getDateString` and the type guards behave identically; `predefinedFormatters` gains @@ -29,4 +29,4 @@ export { isDayOrMoment, isNumberOrString, predefinedFormatters, -} from 'stream-chat/i18n'; +} from '@stream-io/i18n'; From 50065cd232a7c51072ac1801201bb7854e643336 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Thu, 10 Sep 2026 15:27:53 +0200 Subject: [PATCH 2/6] refactor(store)!: import StateStore from @stream-io/state-store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: adds `@stream-io/state-store` as a dependency. `stream-chat` no longer re-exports `StateStore`, so the 22 modules that imported it from there now take it from the package that defines it. No behaviour change — the store instances are the same objects, only the import specifier moves. A single shared declaration matters here: TypeScript compares classes with `protected` members nominally, so two copies would not be assignable to one another and `useStateStore` would stop typechecking against stores handed over by `stream-chat` or `@stream-io/i18n`. --- package.json | 1 + src/components/AudioPlayback/AudioPlayer.ts | 2 +- src/components/AudioPlayback/AudioPlayerPool.ts | 2 +- src/components/Dialog/service/DialogManager.ts | 2 +- .../InfiniteScrollPaginator/InfiniteScrollWithComponents.tsx | 3 ++- .../InfiniteScrollPaginator/hooks/useCursorPaginator.ts | 2 +- .../hooks/__tests__/useMessageComposerCommands.test.tsx | 2 +- src/components/MessageList/__tests__/MessageList.test.tsx | 3 ++- .../MessageList/__tests__/ScrollToLatestMessageButton.test.tsx | 2 +- .../Notifications/hooks/__tests__/useNotificationApi.test.ts | 2 +- .../Notifications/hooks/__tests__/useNotificationApi.test.tsx | 2 +- .../Notifications/hooks/__tests__/useNotifications.test.tsx | 2 +- src/components/Thread/__tests__/Thread.test.tsx | 2 +- src/components/Thread/__tests__/ThreadStart.test.tsx | 2 +- src/context/DialogManagerContext.tsx | 2 +- src/mock-builders/generator/channelState.ts | 3 ++- .../PinnedMessagesView/__tests__/PinnedMessagesView.test.tsx | 3 ++- .../__tests__/usePinnedMessagesCount.test.ts | 2 +- src/plugins/SlotLayout/__tests__/ChatViewNavigation.test.tsx | 2 +- src/plugins/SlotLayout/__tests__/useSlotEntity.test.tsx | 2 +- src/plugins/SlotLayout/layoutController/LayoutController.ts | 2 +- .../SlotLayout/layoutController/layoutControllerTypes.ts | 2 +- src/store/hooks/useStateStore.ts | 2 +- 23 files changed, 27 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index e375a6f33..41106dbac 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "@floating-ui/react": "^0.27.19", "@react-aria/focus": "^3.22.0", "@stream-io/i18n": "^1.0.0", + "@stream-io/state-store": "^1.1.6", "clsx": "^2.1.1", "emoji-regex": "^9.2.2", "fix-webm-duration": "^1.0.6", diff --git a/src/components/AudioPlayback/AudioPlayer.ts b/src/components/AudioPlayback/AudioPlayer.ts index 8e29b675b..103ef9bf3 100644 --- a/src/components/AudioPlayback/AudioPlayer.ts +++ b/src/components/AudioPlayback/AudioPlayer.ts @@ -1,4 +1,4 @@ -import { StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; import throttle from 'lodash.throttle'; import type { AudioPlayerPlugin } from './plugins'; import type { AudioPlayerPool } from './AudioPlayerPool'; diff --git a/src/components/AudioPlayback/AudioPlayerPool.ts b/src/components/AudioPlayback/AudioPlayerPool.ts index f5936daa4..aa9ce6e53 100644 --- a/src/components/AudioPlayback/AudioPlayerPool.ts +++ b/src/components/AudioPlayback/AudioPlayerPool.ts @@ -1,5 +1,5 @@ import { AudioPlayer, type AudioPlayerOptions } from './AudioPlayer'; -import { StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; export type AudioPlayerPoolState = { activeAudioPlayer: AudioPlayer | null; diff --git a/src/components/Dialog/service/DialogManager.ts b/src/components/Dialog/service/DialogManager.ts index 8157d6725..a4814e49f 100644 --- a/src/components/Dialog/service/DialogManager.ts +++ b/src/components/Dialog/service/DialogManager.ts @@ -1,5 +1,5 @@ import { nanoid } from 'nanoid'; -import { StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; export type GetDialogParams = { id: DialogId; diff --git a/src/components/InfiniteScrollPaginator/InfiniteScrollWithComponents.tsx b/src/components/InfiniteScrollPaginator/InfiniteScrollWithComponents.tsx index 15f12aa68..191e9e3ba 100644 --- a/src/components/InfiniteScrollPaginator/InfiniteScrollWithComponents.tsx +++ b/src/components/InfiniteScrollPaginator/InfiniteScrollWithComponents.tsx @@ -1,6 +1,7 @@ import type { ComponentType } from 'react'; import React, { forwardRef } from 'react'; -import type { PaginatorState, StateStore } from 'stream-chat'; +import type { StateStore } from '@stream-io/state-store'; +import type { PaginatorState } from 'stream-chat'; import { useStateStore } from '../../store'; import type { InfiniteScrollPaginatorProps } from './InfiniteScrollPaginator'; diff --git a/src/components/InfiniteScrollPaginator/hooks/useCursorPaginator.ts b/src/components/InfiniteScrollPaginator/hooks/useCursorPaginator.ts index 6a2ec94bd..d109c59f4 100644 --- a/src/components/InfiniteScrollPaginator/hooks/useCursorPaginator.ts +++ b/src/components/InfiniteScrollPaginator/hooks/useCursorPaginator.ts @@ -1,6 +1,6 @@ import uniqBy from 'lodash.uniqby'; import { useCallback, useEffect, useMemo } from 'react'; -import { StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; export type CursorPaginatorState = { hasNextPage: boolean; diff --git a/src/components/MessageComposer/hooks/__tests__/useMessageComposerCommands.test.tsx b/src/components/MessageComposer/hooks/__tests__/useMessageComposerCommands.test.tsx index db770df32..6a15c112d 100644 --- a/src/components/MessageComposer/hooks/__tests__/useMessageComposerCommands.test.tsx +++ b/src/components/MessageComposer/hooks/__tests__/useMessageComposerCommands.test.tsx @@ -1,7 +1,7 @@ import { act, renderHook } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import type { ChannelConfig, Command, MessageComposerState } from 'stream-chat'; -import { StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; import { useMessageComposerCommands } from '../useMessageComposerCommands'; diff --git a/src/components/MessageList/__tests__/MessageList.test.tsx b/src/components/MessageList/__tests__/MessageList.test.tsx index 8d4cec074..7c37a44d4 100644 --- a/src/components/MessageList/__tests__/MessageList.test.tsx +++ b/src/components/MessageList/__tests__/MessageList.test.tsx @@ -24,7 +24,8 @@ import { useChannel, useMessageContext, WithComponents } from '../../../context' import { EmptyStateIndicator as EmptyStateIndicatorMock } from '../../EmptyStateIndicator'; import { mockedApiResponse } from '../../../mock-builders/api/utils'; import { nanoid } from 'nanoid'; -import { msToNs, StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; +import { msToNs } from 'stream-chat'; import type { Channel as ChannelType, Event, diff --git a/src/components/MessageList/__tests__/ScrollToLatestMessageButton.test.tsx b/src/components/MessageList/__tests__/ScrollToLatestMessageButton.test.tsx index 764c1aea6..11eec46ce 100644 --- a/src/components/MessageList/__tests__/ScrollToLatestMessageButton.test.tsx +++ b/src/components/MessageList/__tests__/ScrollToLatestMessageButton.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; import type { Channel, StreamChat, Thread, UserResponse } from 'stream-chat'; import { ScrollToLatestMessageButton } from '../ScrollToLatestMessageButton'; diff --git a/src/components/Notifications/hooks/__tests__/useNotificationApi.test.ts b/src/components/Notifications/hooks/__tests__/useNotificationApi.test.ts index 88b56f201..93f8c33d9 100644 --- a/src/components/Notifications/hooks/__tests__/useNotificationApi.test.ts +++ b/src/components/Notifications/hooks/__tests__/useNotificationApi.test.ts @@ -1,6 +1,6 @@ import { renderHook } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; -import { StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; import { useChatContext, useModalDialogManager } from '../../../../context'; import { modalDialogId } from '../../../Dialog'; diff --git a/src/components/Notifications/hooks/__tests__/useNotificationApi.test.tsx b/src/components/Notifications/hooks/__tests__/useNotificationApi.test.tsx index b137c40b6..67ebf64ef 100644 --- a/src/components/Notifications/hooks/__tests__/useNotificationApi.test.tsx +++ b/src/components/Notifications/hooks/__tests__/useNotificationApi.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { renderHook, waitFor } from '@testing-library/react'; -import { StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; import { GlobalModal } from '../../../Modal'; import { useNotificationApi } from '../useNotificationApi'; diff --git a/src/components/Notifications/hooks/__tests__/useNotifications.test.tsx b/src/components/Notifications/hooks/__tests__/useNotifications.test.tsx index 1c658f693..c606b7611 100644 --- a/src/components/Notifications/hooks/__tests__/useNotifications.test.tsx +++ b/src/components/Notifications/hooks/__tests__/useNotifications.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { renderHook } from '@testing-library/react'; -import { StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; import { useNotifications } from '../useNotifications'; import { NotificationConfigurationProvider } from '../../NotificationConfigurationContext'; diff --git a/src/components/Thread/__tests__/Thread.test.tsx b/src/components/Thread/__tests__/Thread.test.tsx index 5843dc933..736bf993d 100644 --- a/src/components/Thread/__tests__/Thread.test.tsx +++ b/src/components/Thread/__tests__/Thread.test.tsx @@ -1,7 +1,7 @@ import { act, cleanup, fireEvent, render, waitFor } from '@testing-library/react'; import React from 'react'; import { fromPartial } from '@total-typescript/shoehorn'; -import { StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; import type { ChannelConfig, LocalMessage, diff --git a/src/components/Thread/__tests__/ThreadStart.test.tsx b/src/components/Thread/__tests__/ThreadStart.test.tsx index 86e3b5e00..518a0f86d 100644 --- a/src/components/Thread/__tests__/ThreadStart.test.tsx +++ b/src/components/Thread/__tests__/ThreadStart.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { cleanup, render, screen } from '@testing-library/react'; -import { StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; import { fromPartial } from '@total-typescript/shoehorn'; import { ThreadStart } from '../ThreadStart'; diff --git a/src/context/DialogManagerContext.tsx b/src/context/DialogManagerContext.tsx index 71edcea19..bae1b76a9 100644 --- a/src/context/DialogManagerContext.tsx +++ b/src/context/DialogManagerContext.tsx @@ -5,7 +5,7 @@ import React, { useMemo, useState, } from 'react'; -import { StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; import { DialogManager } from '../components/Dialog/service/DialogManager'; import { diff --git a/src/mock-builders/generator/channelState.ts b/src/mock-builders/generator/channelState.ts index daeb7b2c3..2b2e9d865 100644 --- a/src/mock-builders/generator/channelState.ts +++ b/src/mock-builders/generator/channelState.ts @@ -1,4 +1,5 @@ -import { AIStates, ChannelWatchStatus, StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; +import { AIStates, ChannelWatchStatus } from 'stream-chat'; import type { ChannelMemberResponse, ChannelState, ChannelStateData } from 'stream-chat'; /** The non-reactive convenience getters the real `ChannelState` layers over its own store. */ diff --git a/src/plugins/ChannelDetail/Views/PinnedMessagesView/__tests__/PinnedMessagesView.test.tsx b/src/plugins/ChannelDetail/Views/PinnedMessagesView/__tests__/PinnedMessagesView.test.tsx index 8ce4efe61..f4d60279e 100644 --- a/src/plugins/ChannelDetail/Views/PinnedMessagesView/__tests__/PinnedMessagesView.test.tsx +++ b/src/plugins/ChannelDetail/Views/PinnedMessagesView/__tests__/PinnedMessagesView.test.tsx @@ -1,6 +1,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; -import { msToNs, StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; +import { msToNs } from 'stream-chat'; import type { Channel, LocalMessage, diff --git a/src/plugins/ChannelDetail/Views/PinnedMessagesView/__tests__/usePinnedMessagesCount.test.ts b/src/plugins/ChannelDetail/Views/PinnedMessagesView/__tests__/usePinnedMessagesCount.test.ts index bc5538c6a..09806665d 100644 --- a/src/plugins/ChannelDetail/Views/PinnedMessagesView/__tests__/usePinnedMessagesCount.test.ts +++ b/src/plugins/ChannelDetail/Views/PinnedMessagesView/__tests__/usePinnedMessagesCount.test.ts @@ -1,6 +1,6 @@ import { act, renderHook } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; -import { StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; import type { Channel, LocalMessage, PaginatorState } from 'stream-chat'; import { usePinnedMessagesCount } from '../usePinnedMessagesCount'; diff --git a/src/plugins/SlotLayout/__tests__/ChatViewNavigation.test.tsx b/src/plugins/SlotLayout/__tests__/ChatViewNavigation.test.tsx index 54f315ecd..9579aa114 100644 --- a/src/plugins/SlotLayout/__tests__/ChatViewNavigation.test.tsx +++ b/src/plugins/SlotLayout/__tests__/ChatViewNavigation.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import type { TranslationContextValue } from '../../../context/TranslationContext'; -import { StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; import { fireEvent, render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; diff --git a/src/plugins/SlotLayout/__tests__/useSlotEntity.test.tsx b/src/plugins/SlotLayout/__tests__/useSlotEntity.test.tsx index 9c1a0f1da..ba16ff57c 100644 --- a/src/plugins/SlotLayout/__tests__/useSlotEntity.test.tsx +++ b/src/plugins/SlotLayout/__tests__/useSlotEntity.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import type { TranslationContextValue } from '../../../context/TranslationContext'; -import { StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; import { render, screen, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; diff --git a/src/plugins/SlotLayout/layoutController/LayoutController.ts b/src/plugins/SlotLayout/layoutController/LayoutController.ts index 3fab58e76..7509e0960 100644 --- a/src/plugins/SlotLayout/layoutController/LayoutController.ts +++ b/src/plugins/SlotLayout/layoutController/LayoutController.ts @@ -1,4 +1,4 @@ -import { StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; import type { ChatViewLayoutState, diff --git a/src/plugins/SlotLayout/layoutController/layoutControllerTypes.ts b/src/plugins/SlotLayout/layoutController/layoutControllerTypes.ts index 034e4d779..de3b33275 100644 --- a/src/plugins/SlotLayout/layoutController/layoutControllerTypes.ts +++ b/src/plugins/SlotLayout/layoutController/layoutControllerTypes.ts @@ -1,4 +1,4 @@ -import type { StateStore } from 'stream-chat'; +import type { StateStore } from '@stream-io/state-store'; import type { ChatView } from '../ChatView'; diff --git a/src/store/hooks/useStateStore.ts b/src/store/hooks/useStateStore.ts index cc7a326d4..73ad55d40 100644 --- a/src/store/hooks/useStateStore.ts +++ b/src/store/hooks/useStateStore.ts @@ -1,7 +1,7 @@ import { useCallback, useMemo } from 'react'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; -import type { StateStore } from 'stream-chat'; +import type { StateStore } from '@stream-io/state-store'; // eslint-disable-next-line @typescript-eslint/no-empty-function const noop = () => {}; From 5fc496ebe1280c21a71323dffb3e8c8f2f928c3e Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Fri, 11 Sep 2026 14:08:45 +0200 Subject: [PATCH 3/6] fix(examples): import StateStore from @stream-io/state-store The previous commit stopped `stream-chat` from exporting `StateStore`, and updated every call site in `src/` -- but not the Vite example, which is the reference integration and is built and deployed by CI on every PR. `examples/vite/src/AppSettings/state.ts` constructs one at module scope, so this was a runtime break, not just a type error: `StateStore` resolves to `undefined` and the app throws `TypeError: StateStore is not a constructor` on boot. Rollup catches it first -- `[MISSING_EXPORT] "StateStore" is not exported by stream-chat` -- so `yarn build` in the example fails outright. Two related fixes in the example manifests: - `@stream-io/state-store` is now declared, rather than relied on by hoisting. - `stream-chat` was pinned exactly at `10.0.0-rc.9` in both examples, which no longer satisfies this package's `^10.0.0-rc.10` peer range. Loosened to a caret. Also aligns `dayjs` at `^1.11.23` to match `@stream-io/i18n`. The ranges had drifted apart (`^1.11.13` here, `^1.11.20` in the example, `^1.11.23` in the package), and dayjs locale registration is global: a second resolved copy means an integrator's `import 'dayjs/locale/de'` extends an instance the SDK never formats with, and dates stay English with nothing thrown. CLAUDE.md asserted the old range, so it is corrected too. --- CLAUDE.md | 2 +- examples/tutorial/package.json | 2 +- examples/vite/package.json | 5 +++-- examples/vite/src/AppSettings/state.ts | 2 +- package.json | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c42bd838c..ceac2e89d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -344,7 +344,7 @@ Two resolved copies of `i18next` would be harmless anyway — `Streami18n` calls `i18next.createInstance()` and registers dictionaries on that instance, never on a global. `dayjs` is the one that genuinely wants a single copy, because locale registration _is_ global: an app adding a language does `import 'dayjs/locale/de.js'`, and that has to land in the same registry the -SDK reads. Keeping the range compatible (`^1.11.13`) is what makes them dedupe. Do not add +SDK reads. Keeping the range identical (`^1.11.23`) is what makes them dedupe. Do not add `i18next` as a direct dependency here. The `language.*` names (`languageNameDefaults`, `LanguageNameCatalog`) still come from diff --git a/examples/tutorial/package.json b/examples/tutorial/package.json index de3726271..1f3438a5b 100644 --- a/examples/tutorial/package.json +++ b/examples/tutorial/package.json @@ -16,7 +16,7 @@ "emoji-mart": "^5.6.0", "react": "^19.2.6", "react-dom": "^19.2.6", - "stream-chat": "10.0.0-rc.9", + "stream-chat": "^10.0.0-rc.10", "stream-chat-react": "workspace:^" }, "devDependencies": { diff --git a/examples/vite/package.json b/examples/vite/package.json index a755391db..2b35d46c1 100644 --- a/examples/vite/package.json +++ b/examples/vite/package.json @@ -10,14 +10,15 @@ }, "dependencies": { "@emoji-mart/data": "^1.2.1", + "@stream-io/state-store": "^1.1.6", "clsx": "^2.1.1", - "dayjs": "^1.11.20", + "dayjs": "^1.11.23", "emoji-mart": "^5.6.0", "human-id": "^4.1.3", "modern-normalize": "^3.0.1", "react": "^19.2.6", "react-dom": "^19.2.6", - "stream-chat": "10.0.0-rc.9", + "stream-chat": "^10.0.0-rc.10", "stream-chat-react": "workspace:^" }, "devDependencies": { diff --git a/examples/vite/src/AppSettings/state.ts b/examples/vite/src/AppSettings/state.ts index d8e992e0f..812dac611 100644 --- a/examples/vite/src/AppSettings/state.ts +++ b/examples/vite/src/AppSettings/state.ts @@ -1,4 +1,4 @@ -import { StateStore } from 'stream-chat'; +import { StateStore } from '@stream-io/state-store'; import { useStateStore } from 'stream-chat-react'; import { DEFAULT_LANGUAGE, streamI18n } from '../i18n'; diff --git a/package.json b/package.json index 41106dbac..e76d11721 100644 --- a/package.json +++ b/package.json @@ -185,7 +185,7 @@ "@vitest/eslint-plugin": "^1.6.20", "concurrently": "^9.2.1", "conventional-changelog-conventionalcommits": "^9.3.1", - "dayjs": "^1.11.13", + "dayjs": "^1.11.23", "emoji-mart": "^5.6.0", "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", From 2aa3f8b998455cdc12b7ea747bba451e9c1ff455 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Fri, 11 Sep 2026 14:27:18 +0200 Subject: [PATCH 4/6] chore(deps): point @stream-io/i18n at the version that shipped The package released as 1.0.1, not 1.0.0 -- the version PR consumed 1.0.0 while publishing was still broken, so it never reached npm. `^1.0.0` resolves to 1.0.1 today, so this is accuracy rather than a fix: it stops the manifest declaring a floor that does not exist on the registry. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e76d11721..ef0fe83d1 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "@braintree/sanitize-url": "^7.1.2", "@floating-ui/react": "^0.27.19", "@react-aria/focus": "^3.22.0", - "@stream-io/i18n": "^1.0.0", + "@stream-io/i18n": "^1.0.1", "@stream-io/state-store": "^1.1.6", "clsx": "^2.1.1", "emoji-regex": "^9.2.2", From e2b7142b7c0bacd8819d27f8b352d715d2919d8f Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Fri, 11 Sep 2026 14:45:57 +0200 Subject: [PATCH 5/6] build(deps): resolve @stream-io/i18n and stream-chat from npm First lockfile that resolves both packages from the registry rather than from a local `file:` tarball: `@stream-io/i18n@1.0.1` and `stream-chat@10.0.0-rc.10`. No `.local-links` entries remain. `scripts/generate-i18n-keys.mts` switches to `import * as ts from 'typescript'`. The default import stopped satisfying `TypeScriptModule` once the generator moved into `@stream-io/i18n`, because marking `dist/codegen` as `type: module` (so the ESM-only entry point loads on any Node) also makes TypeScript read its `.d.ts` files as ESM. Under `nodenext`, a namespace import of a CJS `export =` module is then modelled with a synthetic `default`, so the exported `typeof ts` requires a `default` property that the default import does not have: error TS2741: Property 'default' is missing in type 'typeof import(".../typescript")' but required in type 'typeof ts' Verified this is specifically the nested manifest: with it removed from node_modules the default import type-checks, with it present it does not. The same probe against stream-chat rc.9's copy of the identical declaration passes, so it is the packaging change rather than the type text. A namespace import is the correct shape here anyway -- it is what the package's own declaration uses -- and `yarn build-translations` regenerates `keys.ts` byte-identically under it. The package-side fix, so the default import keeps working for every consumer, is filed separately. --- scripts/generate-i18n-keys.mts | 2 +- yarn.lock | 78 ++++++++++++++++++++++++---------- 2 files changed, 57 insertions(+), 23 deletions(-) diff --git a/scripts/generate-i18n-keys.mts b/scripts/generate-i18n-keys.mts index 341fac7a5..d799bc860 100644 --- a/scripts/generate-i18n-keys.mts +++ b/scripts/generate-i18n-keys.mts @@ -8,7 +8,7 @@ // // Run by `yarn build-translations`, from the package root — every path below is relative to it. // `yarn validate-translations` runs it and fails on any diff, which is the drift gate. -import ts from 'typescript'; +import * as ts from 'typescript'; import { generateI18nKeys } from '@stream-io/i18n/codegen'; const jsonFlag = process.argv.indexOf('--json'); diff --git a/yarn.lock b/yarn.lock index 479c87a66..d308e1c0b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1811,6 +1811,23 @@ __metadata: languageName: node linkType: hard +"@stream-io/i18n@npm:^1.0.1": + version: 1.0.1 + resolution: "@stream-io/i18n@npm:1.0.1" + dependencies: + "@stream-io/state-store": "npm:^1.1.6" + dayjs: "npm:^1.11.23" + i18next: "npm:^26.4.2" + use-sync-external-store: "npm:^1.7.0" + peerDependencies: + react: ^17 || ^18 || ^19 + peerDependenciesMeta: + react: + optional: true + checksum: 10c0/91e2b76256926314af5fc53ae67069feecda183bbe50fb1c093b36ee8c97d05037715138db283da7bf50668b711410c95bcec29f1d06a4d39bf49575f1a5ec81 + languageName: node + linkType: hard + "@stream-io/logger@npm:^2.0.0": version: 2.0.0 resolution: "@stream-io/logger@npm:2.0.0" @@ -1818,6 +1835,21 @@ __metadata: languageName: node linkType: hard +"@stream-io/state-store@npm:^1.1.6": + version: 1.1.6 + resolution: "@stream-io/state-store@npm:1.1.6" + peerDependencies: + react: ^17 || ^18 || ^19 + use-sync-external-store: ^1.5.0 + peerDependenciesMeta: + react: + optional: true + use-sync-external-store: + optional: true + checksum: 10c0/42927cdb6a5bf6ba5f4ec11bb5e530db0a04edc65abd5348eb423780e40a3e53f57125e328d0318a0dc6b24ca0fcb28e8b91895a14a62ed8d82645f67ebcee5a + languageName: node + linkType: hard + "@stream-io/stream-chat-react-tutorial@workspace:examples/tutorial": version: 0.0.0-use.local resolution: "@stream-io/stream-chat-react-tutorial@workspace:examples/tutorial" @@ -1829,7 +1861,7 @@ __metadata: emoji-mart: "npm:^5.6.0" react: "npm:^19.2.6" react-dom: "npm:^19.2.6" - stream-chat: "npm:10.0.0-rc.9" + stream-chat: "npm:^10.0.0-rc.10" stream-chat-react: "workspace:^" typescript: "npm:^6.0.3" vite: "npm:^8.1.3" @@ -1843,20 +1875,21 @@ __metadata: "@babel/core": "npm:^7.29.0" "@emoji-mart/data": "npm:^1.2.1" "@playwright/test": "npm:^1.60.0" + "@stream-io/state-store": "npm:^1.1.6" "@types/react": "npm:^19.2.15" "@types/react-dom": "npm:^19.2.3" "@vitejs/plugin-react": "npm:^6.0.3" "@vitejs/plugin-react-swc": "npm:^4.3.1" babel-plugin-react-compiler: "npm:^1.0.0" clsx: "npm:^2.1.1" - dayjs: "npm:^1.11.20" + dayjs: "npm:^1.11.23" emoji-mart: "npm:^5.6.0" human-id: "npm:^4.1.3" modern-normalize: "npm:^3.0.1" react: "npm:^19.2.6" react-dom: "npm:^19.2.6" sass: "npm:^1.100.0" - stream-chat: "npm:10.0.0-rc.9" + stream-chat: "npm:^10.0.0-rc.10" stream-chat-react: "workspace:^" typescript: "npm:^6.0.3" vite: "npm:^8.1.3" @@ -3752,7 +3785,7 @@ __metadata: languageName: node linkType: hard -"dayjs@npm:^1.11.13, dayjs@npm:^1.11.20": +"dayjs@npm:^1.11.23": version: 1.11.23 resolution: "dayjs@npm:1.11.23" checksum: 10c0/69ab04bf19c676e44ab50cc2fca223d265f3dafd107151ef3b0f3ad74d360c37caa602abe62f87cb25d90bd9f6bee003698af47df5b0dbf10a998b7b5f3bb3f1 @@ -5323,15 +5356,15 @@ __metadata: languageName: node linkType: hard -"i18next@npm:^26.3.6": - version: 26.3.6 - resolution: "i18next@npm:26.3.6" +"i18next@npm:^26.4.2": + version: 26.4.2 + resolution: "i18next@npm:26.4.2" peerDependencies: typescript: ^5 || ^6 || ^7 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/5920ac8fb6b647a2bdd439d121de04e5297246e52c4d95d13f5faae0824fd45b2faeb66038bcd61fc62362daa815f9c32fb992d6faa3787607d1dfe768e9a8b1 + checksum: 10c0/f2c9613e03e09e56a84aee939d8fbe3442a58622915e11f97e47f9ab358dfa65be783230b37a21483152711f786fccfe697d7f5ea9269c54f16179dc59960e4b languageName: node linkType: hard @@ -9461,6 +9494,8 @@ __metadata: "@semantic-release/changelog": "npm:^6.0.3" "@semantic-release/git": "npm:^10.0.1" "@semantic-release/github": "npm:^12.0.8" + "@stream-io/i18n": "npm:^1.0.1" + "@stream-io/state-store": "npm:^1.1.6" "@stream-io/transliterate": "npm:^1.5.5" "@testing-library/dom": "npm:^10.4.1" "@testing-library/jest-dom": "npm:^6.9.1" @@ -9482,7 +9517,7 @@ __metadata: clsx: "npm:^2.1.1" concurrently: "npm:^9.2.1" conventional-changelog-conventionalcommits: "npm:^9.3.1" - dayjs: "npm:^1.11.13" + dayjs: "npm:^1.11.23" emoji-mart: "npm:^5.6.0" emoji-regex: "npm:^9.2.2" eslint: "npm:^9.39.4" @@ -9516,13 +9551,13 @@ __metadata: remark-parse: "npm:^11.0.0" sass: "npm:^1.100.0" semantic-release: "npm:^25.0.3" - stream-chat: "npm:10.0.0-rc.9" + stream-chat: "npm:^10.0.0-rc.10" typescript: "npm:^6.0.3" typescript-eslint: "npm:^8.59.4" unified: "npm:^11.0.5" unist-builder: "npm:^4.0.0" unist-util-visit: "npm:^5.1.0" - use-sync-external-store: "npm:^1.6.0" + use-sync-external-store: "npm:^1.7.0" vite: "npm:^8.1.3" vitest: "npm:^4.1.9" vitest-axe: "npm:^0.1.0" @@ -9534,7 +9569,7 @@ __metadata: modern-normalize: ^3.0.1 react: ^19.0.0 || ^18.0.0 || ^17.0.0 react-dom: ^19.0.0 || ^18.0.0 || ^17.0.0 - stream-chat: 10.0.0-rc.9 + stream-chat: ^10.0.0-rc.10 dependenciesMeta: "@parcel/watcher": built: true @@ -9560,21 +9595,20 @@ __metadata: languageName: unknown linkType: soft -"stream-chat@npm:10.0.0-rc.9": - version: 10.0.0-rc.9 - resolution: "stream-chat@npm:10.0.0-rc.9" +"stream-chat@npm:^10.0.0-rc.10": + version: 10.0.0-rc.10 + resolution: "stream-chat@npm:10.0.0-rc.10" dependencies: "@stream-io/logger": "npm:^2.0.0" + "@stream-io/state-store": "npm:^1.1.6" axios: "npm:^1.19.0" - dayjs: "npm:^1.11.13" - i18next: "npm:^26.3.6" linkifyjs: "npm:^4.3.3" dependenciesMeta: esbuild: built: true husky: built: true - checksum: 10c0/4409003abe465e7d75d5b55daf6a197aeec346a9dcbbb84ad729dc2c31155899b6cf464d3a3be3249cf132b1cb3b67e4e7d0e3356379f22160779581b7b38dd1 + checksum: 10c0/00eb4de2b390f633a7b7e2613a6a2aa30543c6e0bcda96bbf10ac257cef07bd14b78e2c5677ad36c2b91d64d820baff63fa65782581f568755a9984f18843dc0 languageName: node linkType: hard @@ -10501,12 +10535,12 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:^1.6.0": - version: 1.6.0 - resolution: "use-sync-external-store@npm:1.6.0" +"use-sync-external-store@npm:^1.6.0, use-sync-external-store@npm:^1.7.0": + version: 1.7.0 + resolution: "use-sync-external-store@npm:1.7.0" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10c0/35e1179f872a53227bdf8a827f7911da4c37c0f4091c29b76b1e32473d1670ebe7bcd880b808b7549ba9a5605c233350f800ffab963ee4a4ee346ee983b6019b + checksum: 10c0/6bae7cb778ce611131ec830c4e469fbc2872acefc25d2e4cf1c28b8628690689770742a9f2f0642c95134f9c77041fee60f0352f81ca43f96643b0ed01774726 languageName: node linkType: hard From e8c36203d034f72b69f107c29f4b236fa2326793 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Fri, 11 Sep 2026 15:06:51 +0200 Subject: [PATCH 6/6] build(deps): take @stream-io/i18n 1.0.2 and drop the namespace-import workaround 1.0.2 fixes `TypeScriptModule`, which 1.0.1 had declared as `typeof ts` over a namespace import. Because `./codegen` ships a nested manifest marking the directory ESM, TypeScript read its `.d.ts` as ESM, where a namespace import of a CJS `export =` module carries a synthetic `default` -- so the exported type required a property the plain default import does not have. It is now an explicit `Pick` of the members the generator actually uses, which reads the same in every module mode. So `scripts/generate-i18n-keys.mts` goes back to `import ts from 'typescript'`, matching the style it had before the move and the one the React Native SDK uses. `yarn types:scripts` passes with it, and `build-translations` regenerates `keys.ts` byte-identically. --- package.json | 2 +- scripts/generate-i18n-keys.mts | 2 +- yarn.lock | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index ef0fe83d1..9eac7e695 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "@braintree/sanitize-url": "^7.1.2", "@floating-ui/react": "^0.27.19", "@react-aria/focus": "^3.22.0", - "@stream-io/i18n": "^1.0.1", + "@stream-io/i18n": "^1.0.2", "@stream-io/state-store": "^1.1.6", "clsx": "^2.1.1", "emoji-regex": "^9.2.2", diff --git a/scripts/generate-i18n-keys.mts b/scripts/generate-i18n-keys.mts index d799bc860..341fac7a5 100644 --- a/scripts/generate-i18n-keys.mts +++ b/scripts/generate-i18n-keys.mts @@ -8,7 +8,7 @@ // // Run by `yarn build-translations`, from the package root — every path below is relative to it. // `yarn validate-translations` runs it and fails on any diff, which is the drift gate. -import * as ts from 'typescript'; +import ts from 'typescript'; import { generateI18nKeys } from '@stream-io/i18n/codegen'; const jsonFlag = process.argv.indexOf('--json'); diff --git a/yarn.lock b/yarn.lock index d308e1c0b..64559363d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1811,9 +1811,9 @@ __metadata: languageName: node linkType: hard -"@stream-io/i18n@npm:^1.0.1": - version: 1.0.1 - resolution: "@stream-io/i18n@npm:1.0.1" +"@stream-io/i18n@npm:^1.0.2": + version: 1.0.2 + resolution: "@stream-io/i18n@npm:1.0.2" dependencies: "@stream-io/state-store": "npm:^1.1.6" dayjs: "npm:^1.11.23" @@ -1824,7 +1824,7 @@ __metadata: peerDependenciesMeta: react: optional: true - checksum: 10c0/91e2b76256926314af5fc53ae67069feecda183bbe50fb1c093b36ee8c97d05037715138db283da7bf50668b711410c95bcec29f1d06a4d39bf49575f1a5ec81 + checksum: 10c0/4b073bc8b39064ee31cab37f5c6ea6da03db916acf7fe998e0954fee6c458f0784d27678954c4aaa1eaed68eb8556d10e88ad3127e5ae4081c3ef16f747a3cda languageName: node linkType: hard @@ -9494,7 +9494,7 @@ __metadata: "@semantic-release/changelog": "npm:^6.0.3" "@semantic-release/git": "npm:^10.0.1" "@semantic-release/github": "npm:^12.0.8" - "@stream-io/i18n": "npm:^1.0.1" + "@stream-io/i18n": "npm:^1.0.2" "@stream-io/state-store": "npm:^1.1.6" "@stream-io/transliterate": "npm:^1.5.5" "@testing-library/dom": "npm:^10.4.1"