Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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 @@ -897,3 +915,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