Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ permissions:
jobs:
lint:
runs-on: ubuntu-latest
name: ESLint & Prettier
name: ESLint, Prettier & Types
steps:
- uses: actions/checkout@v6

- uses: ./.github/actions/setup-node

- 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
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.*'
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 36 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ yarn test <pattern> # 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
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 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
`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()`.

Expand Down Expand Up @@ -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_
Expand Down
2 changes: 1 addition & 1 deletion ai-docs/i18n-v15-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
2 changes: 1 addition & 1 deletion examples/tutorial/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
5 changes: 3 additions & 2 deletions examples/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion examples/vite/src/AppSettings/state.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@
"@braintree/sanitize-url": "^7.1.2",
"@floating-ui/react": "^0.27.19",
"@react-aria/focus": "^3.22.0",
"@stream-io/i18n": "^1.0.2",
"@stream-io/state-store": "^1.1.6",
"clsx": "^2.1.1",
"emoji-regex": "^9.2.2",
"fix-webm-duration": "^1.0.6",
Expand All @@ -116,7 +118,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"
Expand All @@ -129,7 +131,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": {
Expand Down Expand Up @@ -183,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",
Expand All @@ -199,7 +201,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",
Expand Down Expand Up @@ -227,7 +229,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'",
Expand Down
6 changes: 3 additions & 3 deletions scripts/generate-i18n-keys.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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.'],
Expand Down
2 changes: 1 addition & 1 deletion src/components/AudioPlayback/AudioPlayer.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 1 addition & 1 deletion src/components/AudioPlayback/AudioPlayerPool.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/components/Dialog/service/DialogManager.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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<T> = {
hasNextPage: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
3 changes: 2 additions & 1 deletion src/components/MessageList/__tests__/MessageList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 1 addition & 1 deletion src/components/Thread/__tests__/Thread.test.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/components/Thread/__tests__/ThreadStart.test.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 1 addition & 1 deletion src/context/DialogManagerContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading