diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml index 0f561fc31f..04ed34cfed 100644 --- a/.github/workflows/check-pr.yml +++ b/.github/workflows/check-pr.yml @@ -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] diff --git a/.github/workflows/sdk-size-metrics.yml b/.github/workflows/sdk-size-metrics.yml index e201f6d94b..73982a2eda 100644 --- a/.github/workflows/sdk-size-metrics.yml +++ b/.github/workflows/sdk-size-metrics.yml @@ -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 diff --git a/AGENTS.md b/AGENTS.md index e86f1bff35..8097495dec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 @@ -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` — 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 diff --git a/ai-docs/accessibility.md b/ai-docs/accessibility.md index 3a460f397a..5bb42114b5 100644 --- a/ai-docs/accessibility.md +++ b/ai-docs/accessibility.md @@ -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 @@ -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', }); @@ -65,12 +66,24 @@ i18n.registerTranslation('nl', { ``` -`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 -