Skip to content
Open
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
3 changes: 3 additions & 0 deletions .github/workflows/check-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
branches:
- develop
- main
# The v10 integration branch. Without it, no PR stacked onto V10 runs build, lint, typecheck or
# tests — every gate is manual for the whole release.
- V10
- 'v[0-9]+.[0-9]+.[0-9]+*beta*'
types: [opened, synchronize]

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/sdk-size-metrics.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ on:
branches:
- develop
- main
# Needed for the same reason as in check-pr.yml, and specifically for this initiative: moving the
# i18n runtime out of `lib/` changes both numbers in the migration guide's size table, and this
# workflow is what measures them.
- V10

env:
HOMEBREW_NO_INSTALL_CLEANUP: 1 # Disable cleanup for homebrew, we don't need it on CI
Expand Down
34 changes: 28 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Respect repo-specific rules. Do not suppress lint rules broadly; justify and sco
- `state-store/` — client-side stores on `useSyncExternalStore` with a selector pattern (audio player, video player, image gallery, message overlay, attachment picker, …)
- `store/` — offline SQLite persistence: `OfflineDB.ts`, `SqliteClient.ts`, `schema.ts`, `mappers/`, `apis/`
- `theme/` — theming system + `topologicalResolution.ts` + `generated/` tokens
- `i18n/` — the translation key layer: generated `keys.ts` catalog, `types.ts`, `runtimeDefaults.ts`, `externalStrings.ts`. No locale JSON — the SDK ships English only (the `Streami18n` wrapper class lives in `utils/i18n/`)
- `i18n/` — the translation key layer: generated `keys.ts` catalog, `types.ts`, `runtimeDefaults.ts`, `utils.ts`. No locale JSON — the SDK ships English only. The runtime lives in `stream-chat/i18n`, shared with the React SDK; `utils/i18n/Streami18n.ts` is a thin subclass injecting this package's bundled data
- `a11y/` — accessibility primitives (`a11yUtils.ts`, `hooks/`)
- `middlewares/` — command UI middlewares (`attachments.ts`, `emojiControl.ts`)
- `icons/` — SVG icon components
Expand Down Expand Up @@ -278,16 +278,38 @@ Integrators add languages additively — there is nothing in the SDK to fork or
`package/src/i18n/runtimeDefaults.ts` — the only translation data that ships. `runtimeDefaults`
holds just the keys with no inline copy to fall back on: `timestamp.*` / `duration.*` formatter
expressions passed around as prop values, and keys built from a runtime value.
- `Streami18n` (`package/src/utils/i18n/Streami18n.ts`) wraps i18next; access `t` via
- **The runtime is `stream-chat/i18n`**, shared with the React SDK — one `Streami18n`, one set of
formatters, one date layer. `package/src/utils/i18n/Streami18n.ts` is a ~30-line subclass that
injects this package's `runtimeDefaults` (core cannot import them: the catalog is generated from
*this* package's call sites). A behavioural fix belongs in `stream-chat`, not here. Access `t` via
`useTranslationContext()`. `registerTranslation` **merges**, so a partial dictionary can never
knock out the bundled formatter keys.
- **Reactivity is a `StateStore`,** not listeners. `i18n.state` publishes
`{ t, tDateTimeParser, language, initialized }`; `useStreami18n` subscribes with a module-scope
selector. `setLanguage()` returns `void` — the new `t` arrives through the store. There is no
`addOnLanguageChangeListener`, and `getTranslators()` is now `init()`. **Nothing is kept as a
deprecated alias** — v10 is a breaking release, so an old name is removed rather than carried with a
countdown on it. `relativeCompactDateFormatter` is gone the same way: use `timestampFormatter` with
`relativeCompact: true`, whose wording goes through `t()`.
- `language.*` (ISO language names) and `relativeTime.*` are typed into the catalog but come from
core, which owns the code that renders them. Neither is declared in `runtimeDefaults`.
- Notification copy is keyed on `stream-chat`'s `CORE_NOTIFICATION_TYPE`, in
`components/Notifications/notificationTranslations.ts`, as a `Record<CoreNotificationType, …>` — so
a new core identifier is a compile error until it is mapped. Never match on `notification.message`;
that is untranslated English whose wording is not part of core's contract. Poll field errors are
keyed the same way, on `POLL_COMPOSER_VALIDATION_CODE`.
- **`dayjs` must resolve to exactly one copy.** Its range here has to stay compatible with core's
(`^1.11.13`) — an exact pin installs a second copy, `instanceof Dayjs` starts failing, and an
integrator's `import 'dayjs/locale/de'` lands on an instance the SDK never formats with. Do not
declare `i18next` at all; it arrives through `stream-chat`.
- Only the `en` dayjs locale is bundled, and **no dayjs locale defines `calendar`** (that field
belongs to the calendar plugin) — a new language needs both `import 'dayjs/locale/xx'` and a
`calendar` config, or relative dates render English scaffolding around translated day names.
- Generation: `build-translations` runs `package/scripts/generate-i18n-keys.mts`, which enforces
five hard-fail guards (conflicting inline copy, unresolvable key, shadowed key, external-string
drift, strict prefix). Its fixture tests are **not** jest — run
`cd package && node --test scripts/tests/i18n-tooling-checks.mts`.
- Generation: `build-translations` runs `package/scripts/generate-i18n-keys.mts`, now a ~40-line
config shim over the generator in `stream-chat/i18n/codegen` (also shared with the React SDK). Four
hard-fail guards: conflicting inline copy, unresolvable key, shadowed key, strict prefix. The
external-string drift guard is gone with `externalStrings.ts`. The fixture tests live in
`stream-chat`'s own suite — there is no longer a `node --test` step here.
- Validation: `validate-translations` runs inside `yarn lint` and in CI. It is a **drift gate** —
it regenerates `keys.ts` and fails if the result differs from what is committed.
- Adding a string: call `t('some.dotted.key', 'English copy')` → run `build-translations` → commit
Expand Down
29 changes: 21 additions & 8 deletions ai-docs/accessibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ When `enabled` is false:
- `useIncomingMessageAnnouncements` does not subscribe to `channel.on('message.new')`.
- No `AccessibilityInfo` event listeners attach.
- Components still render their `accessibilityRole` / `accessibilityState` / etc. attributes (these are passed to native views and only consulted by VO/TalkBack when active — sighted users incur ~zero cost).
- `useA11yLabel(key, params)` returns `undefined` so `t('a11y/...')` is **not** called on hot list paths.
- `useA11yLabel(key, params)` returns `undefined` so `t()` is **not** called on hot list paths.

## Configuration shape

Expand All @@ -50,13 +50,14 @@ For RN-specific gesture-alternative toggles, the enum semantics are:

## Localization

All a11y strings flow through the existing `Streami18n` translation pipeline under the `a11y/*` namespace. Defaults ship in English in every locale; integrators can override per-key via the same mechanism they use for other strings:
All a11y strings flow through the `Streami18n` translation pipeline, the same one every other string uses.
Integrators override per key:

```ts
const i18n = new Streami18n('nl');
const i18n = new Streami18n({ language: 'nl' });
i18n.registerTranslation('nl', {
'a11y/Avatar of {{name}}': 'Avatar van {{name}}',
'a11y/{{count}} new messages': '{{count}} nieuwe berichten',
'avatar.accessibilityLabel': 'Avatar van {{name}}',
'messageList.scrollToBottom.withCount.accessibilityLabel': '{{count}} nieuwe berichten',
});
<OverlayProvider accessibility={{ enabled: true }} i18nInstance={i18n}>
<Chat client={client} i18nInstance={i18n}>
Expand All @@ -65,12 +66,24 @@ i18n.registerTranslation('nl', {
</OverlayProvider>
```

`validate-translations` (run as part of `yarn lint`) enforces non-empty values for every `a11y/*` key in every locale.
Three things changed in v10, and the old form above fails quietly rather than erroring:

- **The `a11y/*` namespace is gone.** Accessible names are the `.accessibilityLabel` leaf of the owning
component's key (`avatar.accessibilityLabel`), so a11y copy sits beside the visible copy it describes. See
`ai-docs/i18n-v10-migration.md` for the full old→new table.
- **The constructor takes an options object**, not a positional language string.
- **English is the only bundled language**, so "in every locale" no longer applies — a key you do not supply
renders its English copy from the inline `defaultValue` at the call site, never a raw dotted path.

Most accessible names reach `t()` as a prop or a lookup value rather than a literal, so they have no inline
default and live in `package/src/i18n/runtimeDefaults.ts` instead. `validate-translations` (part of `yarn lint`)
is a drift gate on the generated catalog: it regenerates `src/i18n/keys.ts` and fails on any difference, which
is what catches a key that was renamed at the call site but not in `runtimeDefaults`.

SDK-owned `Button` components can translate their own accessible names from an i18n key:

```tsx
<Button accessibilityLabelKey='a11y/Send message' iconOnly {...buttonProps} />
<Button accessibilityLabelKey='messageInput.sendMessage.accessibilityLabel' iconOnly {...buttonProps} />
```

Use `accessibilityLabelParams` for interpolated labels. SDK-owned buttons should pass the key/params only. When migrating an already-released button, keep the translation value aligned with the existing label unless the label change is intentionally breaking.
Expand All @@ -92,7 +105,7 @@ Importable from `stream-chat-react-native`:

## Cross-SDK parity

API shapes mirror [`stream-chat-react#3146`](https://github.com/GetStream/stream-chat-react/pull/3146) wherever the platforms agree (`useAccessibilityAnnouncer` ≈ `useAriaLiveAnnouncer`, `useIncomingMessageAnnouncements` ≈ identical params and throttle semantics, `a11y/*` i18n namespace shared). Mobile-only deviations:
API shapes mirror [`stream-chat-react#3146`](https://github.com/GetStream/stream-chat-react/pull/3146) wherever the platforms agree (`useAccessibilityAnnouncer` ≈ `useAriaLiveAnnouncer`, `useIncomingMessageAnnouncements` ≈ identical params and throttle semantics; both SDKs now key accessible names as the `.accessibilityLabel` leaf of the owning component rather than a shared `a11y/*` namespace). Mobile-only deviations:

- `<OverlayProvider accessibility={...}>` config object — RN needs gesture-alternative toggles (audio hold-to-record, gallery pinch/pan) that don't exist on web.
- No `<VisuallyHidden>`, no `<SkipNavigation>`, no roving-focus utilities — RN announcer is imperative, mobile has no Tab key.
Expand Down
48 changes: 48 additions & 0 deletions ai-docs/ai-migration-v9-to-v10.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ rg '\b(sendMessage|SendMessageDisallowedIndicator)\b' src/
rg '\b(onAddedToChannel|onRemovedFromChannel|onChannelDeleted|onChannelHidden|onChannelVisible|onChannelUpdated|onChannelTruncated|onChannelMemberUpdated|onNewMessage|onNewMessageNotification|ChannelListEventHandler|useChannelUpdated|queryChannelsOverride)\b' src/
rg 'useChatContext\(\)' -A6 src/ | rg '\bchannelManager\b'
rg '<Chat\b' -A10 src/ | rg '\bchannelManager\b'

# §19 — i18n: keys, the Streami18n API, and the a11y namespace
rg '\bStreami18n\b|registerTranslation|translationsForLanguage|setLanguage|getTranslators' src/
rg "t\(\s*'a11y/|'[A-Z][a-z]+ [a-z]" src/ # v9 keys WERE the English copy
rg '\b(enTranslations|deTranslations|frTranslations|esTranslations|itTranslations|nlTranslations|ptBrTranslations|ruTranslations|trTranslations|jaTranslations|koTranslations|hiTranslations|heTranslations|arTranslations)\b' src/
rg 'getDateString|getDateStringForA11y' -A4 src/ | rg '\bdate:'
```

---
Expand Down Expand Up @@ -137,6 +143,18 @@ means changed. Details in the linked section.
| `useChannelUpdated()` | removed — `channel.updated` is handled by the orchestrator | §18.2 |
| `loadNextPage(filters, sort, options)` | `loadNextPage()` (no args) | §18.3 |
| `<Chat channelManager={…}>` / `useChatContext().channelManager` | `client.channelManager` | §18.4 |
| `t('Send Message')` — the key *was* the English copy | `t('messageInput.sendMessage.accessibilityLabel', 'Send Message')` — stable dotted key, copy inline | §19 |
| `t('a11y/Send message')` | the `.accessibilityLabel` leaf of the owning component's key | §19 |
| `import { deTranslations } from 'stream-chat-react-native'` | removed — English is the only bundled language; supply your own dictionary | §19 |
| `new Streami18n('nl')` | `new Streami18n({ language: 'nl' })` — options object; the class name is unchanged | §19 |
| `new Streami18n(opts, i18nextConfig)` | `new Streami18n({ ...opts, i18nextConfigOverrides: i18nextConfig })` | §19 |
| `const t = await i18n.setLanguage('de')` | `await i18n.setLanguage('de')` → `void`; read `i18n.t` or subscribe to `i18n.state` | §19 |
| `i18n.addOnLanguageChangeListener(fn)` | `i18n.state.subscribeWithSelector(({ t }) => ({ t }), fn)` | §19 |
| `i18n.getTranslators()` | `i18n.init()` — removed, not aliased; same return value | §19 |
| `{{ timestamp \| relativeCompactDateFormatter }}` | `{{ timestamp \| timestampFormatter(relativeCompact: true) }}` | §19 |
| `getDateString({ date, … })` | `getDateString({ messageCreatedAt, … })`; returns `null`, not `undefined`, when unrenderable | §19 |
| `getDateStringForA11y({ date, … })` | `getCalendarDateStringForA11y({ messageCreatedAt, … })` — same behaviour under a new name | §19 |
| poll `state.errors.x` as an English string | a `PollComposerValidationError` — key your copy on `error.code`, fall back to `error.message` | §19 |

---

Expand Down Expand Up @@ -894,3 +912,33 @@ Configure the shared manager through its own API (`client.channelManager.setEven
`channel.updated`; pull-to-refresh and reconnect (the list re-queries, no
blank); and confirm a pinned-first `sort` keeps pinned channels on top when
other channels receive messages.

---

# Part I — i18n

## 19. English-only bundle, dotted keys, shared runtime

**The full guide is `ai-docs/i18n-v10-migration.md`.** Read it for the migration; this section exists so an
agent grepping this file finds i18n at all, and knows the three shapes of change:

1. **Keys are stable dotted identifiers**, not the English copy, with the English inline as i18next's
`defaultValue`. The reviewed old→new table is `ai-docs/i18n-v10-key-map.json` (389 rows). Renaming is not
optional and **it fails silently** — an old key simply never matches, so the override stops applying and
English renders with no error. Type your dictionary as `TranslationDictionary` to turn that into a compile
error.
2. **English is the only bundled language.** The 12 non-English dictionaries and the `*Translations` exports
are gone. An integrator supplies their own, additively; a key they omit still renders English.
3. **The runtime moved to `stream-chat/i18n`**, shared with the React SDK. Imports are unchanged — everything
is still exported from `stream-chat-react-native` / `stream-chat-expo`, and `Streami18n` keeps its name
— but reactivity is a `StateStore` rather than listeners, `setLanguage` returns `void`,
`getTranslators()` is now `init()`, and a few date-helper parameters were renamed to the one name both
SDKs use. Nothing is kept as a deprecated alias. See the quick-reference rows above.

Two dependency rules that produce silent breakage rather than errors:

- **`dayjs` must resolve to a single copy.** Declare it compatibly with `stream-chat`'s range (`^1.11.13`) or
not at all. A disagreeing exact pin installs a second copy, and an app's `import 'dayjs/locale/de'` then
extends an instance the SDK never formats with — dates stay English, nothing throws.
- **Do not declare `i18next`.** It arrives through `stream-chat`. Two copies mean dictionaries registered on
one instance and read from the other.
16 changes: 3 additions & 13 deletions ai-docs/i18n-v10-key-map.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$comment": "v9 -> v10 translation key map for stream-chat-react-native. The left-hand side is the old key, which was the English copy itself; the right-hand side is the stable dotted key that replaces it. `plural` entries live in the catalog as <key>_one / <key>_other, while call sites use the bare key and pass `count`. Keys not listed here were dead in v9 and have been removed. Generated keys and their English copy: `yarn i18n:export`.",
"count": 391,
"$comment": "v9 -> v10 translation key map for stream-chat-react-native. The left-hand side is the old key, which was the English copy itself; the right-hand side is the stable dotted key that replaces it. `plural` entries live in the catalog as <key>_one / <key>_other, while call sites use the bare key and pass `count`. Keys not listed here have no v10 equivalent -- either they were dead in v9, or the component/notification that rendered them is gone; see the \"Removed keys\" table in i18n-v10-migration.md. Generated keys and their English copy: `yarn i18n:export`.",
"count": 389,
"keys": {
"+{{count}}": {
"key": "attachment.gallery.moreImages.label",
Expand Down Expand Up @@ -517,11 +517,6 @@
"prose": true,
"plural": false
},
"Failed to retrieve location": {
"key": "notifications.locationRetrieveFailed.error",
"prose": true,
"plural": false
},
"Failed to share location": {
"key": "notifications.locationShareFailed.error",
"prose": true,
Expand Down Expand Up @@ -1132,11 +1127,6 @@
"prose": true,
"plural": false
},
"Thread has not been found": {
"key": "notifications.threadNotFound.error",
"prose": true,
"plural": false
},
"Typing": {
"key": "channelPreview.typing.label",
"prose": true,
Expand Down Expand Up @@ -1958,4 +1948,4 @@
"plural": true
}
}
}
}
Loading
Loading