feat(i18n)!: adopt the shared @stream-io/i18n runtime - #2436
Conversation
Replaces the hand-rolled i18next wrapper in `react-bindings` with
`@stream-io/i18n`, the translation runtime shared with Stream Chat, and
moves both SDKs from English-sentence translation keys to namespaced
dotted identifiers that carry their English copy inline.
Keys are now `t('participantList.muteAll.label', 'Mute all')`. The
catalog is generated from the call sites themselves, so `en.json` is
gone from both SDKs and drift between code and catalog is structurally
impossible rather than merely discouraged.
Rendered English is unchanged. A mechanical diff of all 192 mapped rows
reports zero unexpected copy changes; the only differences are four
deliberate variable renames.
What this fixes beyond the rename:
- `translationsOverrides` replaced the SDK's dictionary instead of
merging, so an integrator supplying one string dropped all 147 SDK
strings. The prop is now `translations` and registers over the
bundled defaults; partial dictionaries are safe.
- `fallbackLanguage` was accepted and silently dropped on the floor.
Removed - the fallback is each call site's inline English.
- 51 keys were used in code but absent from every catalog, so they were
untranslatable in every language. They are real keys now.
- 11 more strings (the `CallingState` labels and call-stats verdicts)
became translatable for the first time; they used to resolve from a
runtime value with no catalog entry.
BREAKING CHANGE: `StreamI18n`, `StreamI18nProvider`, `translations`,
`translationsOverrides`, `fallbackLanguage`, `TranslationLanguage`,
`TranslationsMap` and `TranslatorFunction` are removed from both SDKs.
Custom dictionaries must be re-keyed - an old key never matches, so the
override silently stops applying. See `ai-docs/i18n-2.0-migration.md`
and `ai-docs/i18n-2.0-key-map.json`.
|
Important Review skippedToo many files! This PR contains 201 files, which is 101 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (201)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Replaces the local portal link used during development. The lockfile now resolves 1.0.1 from npm, so `yarn install --immutable` passes. `.yarnrc.yml` already allowlists the package under npmPreapprovedPackages: `npmMinimalAgeGate: 2d` would otherwise block a version published today.
Type-only release: dist/esm and dist/cjs are byte-identical to 1.0.1. It narrows `TypeScriptModule` in the codegen entry point from `typeof ts` to a `Pick<>` of the parser members it actually uses, which decouples `scripts/generate-i18n-keys.mts` from the exact typescript version each SDK has installed. Catalogs regenerate byte-identically.
Resolves one conflict in VideoPreview.tsx: main added the `createSoundDetector` import while this branch moved `useI18n` out of `@stream-io/video-react-bindings` into the SDK's own `src/i18n` module. Both changes are kept. The design-system commits that landed on main touch the same components this branch re-keyed, but only the import block collided; the `t()` call sites auto-merged cleanly and the generated catalogs are unchanged.
Bundle sizeBuilt package output. Sizes in KB; delta vs
|
…2443) ### 💡 Overview CI failed for RN sample apps as Metro only searched the app and repo root `node_modules`, and yarn nests some deps in neither — so the bundle failed. Happened since #2436 on `@stream-io/state-store`. The Expo apps have been broken longer on `react-native-url-polyfill`. ### 📝 Implementation notes Added `packages/react-native-sdk/node_modules` as a third Metro search path in all three sample apps. Searched last, so `react` and `react-native` still come from the app's own folder and stay single-copy. 🎫 Ticket: https://linear.app/stream/issue/XYZ-123 📑 Docs: https://github.com/GetStream/docs-content/pull/<id> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved dependency resolution for React Native sample apps when bundling the SDK from source. * Prevented duplicate React and React Native instances during Metro builds. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
💡 Overview
Adopts
@stream-io/i18n— the translation runtime we extracted from Chat — across both video SDKs and both dogfood apps, and moves from English-sentence translation keys to namespaced dotted identifiers carrying their English copy inline. Same mechanism as stream-chat-react#3261.The catalog is generated from the
t()call sites themselves, so there is noen.jsonin either SDK any more, and drift between code and catalog is structurally impossible rather than merely discouraged.Rendered English does not change. A mechanical diff of all 192 mapped rows reports zero unexpected copy changes — the only differences are four deliberate variable renames, listed below.
Important
Depends on
@stream-io/i18n@^1.0.1, now published. The lockfile resolves it from npm andyarn install --immutablepasses locally, so CI should be green.📝 Implementation notes
What moved where
packages/react-bindingssheds i18n entirely —StreamI18n,StreamI18nContextand thei18nextdependency are gone. It could not keep them: the key catalog is generated per-SDK from that SDK's own call sites, so it lives downstream of bindings.StreamVideoProvideris now client-only and its props type is renamedStreamVideoProviderProps.src/i18n/— a thinStreami18nsubclass,runtimeDefaults.ts, the generatedkeys.ts, the derived types, and the React context built on core'screateTranslationContextfactory.common.live.label,participantView.screenShare.stop.label), so a customer shipping both platforms writes one dictionary.runtimeDefaultsis empty in both SDKs. The four lookup tables that resolved a key from a runtime value are nowswitchstatements of literalt()calls, which is what makes them translatable at all.Three bugs fixed on the way
translationsOverridesreplaced instead of merged.<StreamVideoProvider translationsOverrides={translations} {...props} />spread props last, so an integrator passing one German string silently dropped all 147 SDK strings. Invisible in English (the oldparseMissingKeyHandlerechoed the key), total in every other language. Both dogfood apps carried a...sdkTranslations.enre-merge to work around it — now deleted.fallbackLanguagewas accepted and dropped on the floor.StreamVideoProvidernever forwarded it. Removed rather than wired up: the fallback is now each call site's inline English, which cannot be forgotten.react-dogfoodalso seeded language fromnavigator.language, sode-DEnever matched the barededictionary — now normalized and clamped.t(\Sign in with ${provider.name}`)** inreact-dogfood` was a template literal matching no key in any dictionary. It has always echoed.Newly translatable
CallStats, plusCancel,More options). They rendered only because the missing-key handler echoed them, so they were untranslatable in every language.CallingStatelabels and three call-stats verdicts — resolved from a runtime value with no catalog entry at all.Deliberate copy changes (the only four)
participantList.anonymousCount.text{{ count }}→{{ anonymousCount }}— the entry has no plural forms, so i18next's magiccountwas searching for forms that do not existlivestream.backstage.participantsJoinedEarly.text{{ formattedCount }};countstays numeric so plural selection works whenhumanize()is on. A count of 1 now renders the singularlobby.footer.otherParticipants.text(RN){{numberOfParticipants}}→{{ count }}livestreamViewer.earlyParticipants.text(RN)t();1now renders "1 participant has joined early" instead of "1 participants…"{{ direction }} fullscreen/picture-in-picturewere composed from two lookups (t('Leave')/t('Enter')filling a slot) — unlocalizable, since word order is fixed. They are now four whole sentences.Also here
react-sdkgets a vitest setup (matchingclientandcodemod) for the catalog render test. The three colocated test files that existed but had never been run by any script are ported fromnode:testto vitest and now run in CI — which surfaced a real bug: their fixtures lackedpublishedTracks, and the filter-object path reads it unguarded, so two cases threw.build-translationsregenerateskeys.tsand the render fixture; a diff fails the build.ai-docs/i18n-2.0-key-map.json(197 rows, both platforms) andai-docs/i18n-2.0-migration.mdfor integrators.✅ Verification
tsc: react-sdk 0, react-native-sdk 0, both dogfoods 0eslint --max-warnings=0andprettier --check: clean acrosspackages/**andsample-apps/**react-sdkvitest: 32/32 across 4 files · RN jest: 11/11 suites, 38/38{{ placeholders }}found none on any screen🔜 Before merge
Merge the companion docs PR alongside this one.
🎫 Ticket: https://linear.app/stream/issue/REACT-1088/replace-natural-language-i18n-keys-with-namespaced-type-checked-ids
📑 Docs: https://github.com/GetStream/docs-content/pull/1592