Internationalization
The SDK includes translation services powered by i18next, supporting:
- Language switching - Change languages dynamically
- Custom translations - Add or override translations in any language
- i18next API access - Full access to the underlying library
English ships with the SDK, written inline at every call site - there is no en.json and no translations export.
The i18n API changed in v2. translationsOverrides is now translations, fallbackLanguage is gone, and every key is a namespaced dotted identifier instead of an English sentence. See Migrating from v1 below.
Integration
Access translation services via the StreamVideo provider using useI18n consumer.
useI18n() returns:
tfunction - Takes a translation key plus its English copy, returns the translation for the active languagetDateTimeParser- A dayjs instance bound to the active language and timezone
Translation keys
Every string the SDK renders is a namespaced dotted key, written at its call site with the English copy alongside it:
t("ringingCall.incoming.title", "Incoming Call...");The second argument is i18next's defaultValue, and it is why English needs no dictionary. A key missing from your dictionary renders that inline English, never the raw dotted key.
Plural keys pass count with one default per plural form:
t("lobby.footer.otherParticipants.text", {
count: numberOfParticipants,
defaultValue_one: "There is {{ count }} more person in the call.",
defaultValue_other: "There are {{ count }} more people in the call.",
});In a dictionary those are written per form - lobby.footer.otherParticipants.text_one, lobby.footer.otherParticipants.text_other, plus _zero / _two / _few / _many where a language needs them.
The React Native catalog is small - the SDK's UI components carry far less copy than the web SDK's. The complete list is the exported TranslationCatalog type, which your editor autocompletes; the SDK repository can also emit a flat translator-facing JSON with yarn i18n:export in packages/react-native-sdk.
Hermes ships a partial ICU, so Intl.PluralRules has no data for most locales. The SDK imports the intl-pluralrules polyfill from its own entry point, so importing @stream-io/video-react-native-sdk is normally enough. If you initialize i18n before that import runs, add import "intl-pluralrules"; as the first line of your entry file. Without it, every count selects the _other form; the SDK warns when the polyfill is missing.
Configuration
The i18n parameters StreamVideo accepts are:
type StreamVideoI18nProps = {
i18nInstance?: Streami18n;
language?: string;
translations?: Record<string, LooseTranslationDictionary>;
};Custom translations
Pass dictionaries keyed by language code to the translations prop.
They are registered over the SDK's built-in copy rather than replacing it. Keys you omit fall back to the English at the call site, so a partial dictionary is safe.
import type { LooseTranslationDictionary } from "@stream-io/video-react-native-sdk";
const translations: Record<string, LooseTranslationDictionary> = {
de: {
"ringingCall.incoming.title": "Eingehender Anruf...",
"common.join.label": "Beitreten",
},
};
const App = () => {
return (
<StreamVideo client={client} language="de" translations={translations}>
{/*...*/}
</StreamVideo>
);
};A dictionary may also carry your own application's keys alongside the SDK's - that is what LooseTranslationDictionary allows.
In v1 this prop was called translationsOverrides, and despite what that page said it replaced the SDK's dictionary instead of merging with it, so every untranslated key rendered as a raw key. That is fixed: translations registers over the SDK's defaults.
Typed dictionaries
| Type | Accepts | Use it when |
|---|---|---|
TranslationDictionary |
only the SDK's keys | the dictionary holds SDK copy only - a typo or a removed key fails the build |
LooseTranslationDictionary |
the SDK's keys plus any other string | one dictionary carries both SDK and application copy |
import type { TranslationDictionary } from "@stream-io/video-react-native-sdk";
export const de: TranslationDictionary = {
"ringingCall.incoming.title": "Eingehender Anruf...",
};TranslationKey (every key t() accepts) and TranslationCatalog (the key to English copy map) are exported too.
Adding a language
Dates and relative times are rendered by dayjs, which needs the language's locale file imported once in your app. Without it, dates render with the English locale and the SDK logs a warning.
import "intl-pluralrules";
import "dayjs/locale/nl";
import { Streami18n, StreamVideo } from "@stream-io/video-react-native-sdk";
const i18n = new Streami18n({ language: "nl" });
i18n.registerTranslation("nl", {
"common.join.label": "Deelnemen",
"participantView.screenShare.stop.label": "Scherm delen stoppen",
});
const App = () => (
<StreamVideo client={client} i18nInstance={i18n}>
{/*...*/}
</StreamVideo>
);No dayjs locale file defines the calendar strings - those belong to the calendar plugin - so a language whose relative dates must read natively needs a calendar config passed as the third argument to registerTranslation.
Provide your own instance of Streami18n
Pass an instance via the i18nInstance prop. StreamVideo adopts and initializes it; it is not swapped out on later renders.
import { Streami18n, StreamVideo } from "@stream-io/video-react-native-sdk";
const i18n = new Streami18n({
language: "en",
translationsForLanguage: {
"ringingCall.incoming.title": "Someone is calling...",
},
});
const App = () => (
<StreamVideo client={client} i18nInstance={i18n}>
{/*...*/}
</StreamVideo>
);Streami18n also accepts timezone, formatters, disableDateTimeTranslations, DateTimeParser (your own dayjs or moment module) and i18nextConfigOverrides, applied over the SDK's i18next init options.
registerTranslation(language, dictionary, dayjsLocaleConfig?) and setLanguage(language) may be called before or after initialization. setLanguage resolves with no value - the new t is published to i18n.state, which the provider subscribes to.
Language
Set the current language with the language prop using language codes (en, de, etc.) matching a translations key or a language registered on your own instance.
const App = () => {
/* a hook that keeps track of the current language in your app */
const { language, setLanguage } = useLanguage();
return (
<StreamVideo
client={client}
language={language}
translations={translations}
>
{/*...*/}
</StreamVideo>
);
};Changing language switches languages in place. A language with no registered dictionary is not an error - the SDK's English copy renders.
Fallback behaviour
There is no fallbackLanguage option. The fallback is the English written inline at every call site, so a key missing from the active dictionary already renders correct English, and i18next's fallbackLng is turned off.
To deliberately fall back from one language to another - a regional dictionary completed by its base language - turn it back on:
const i18n = new Streami18n({
language: "de-AT",
i18nextConfigOverrides: { fallbackLng: "de" },
});Translation function
The t function takes a key and its English copy, and returns the translation for the active language. If the key is not in the active dictionary, the English copy is returned.
Accessing the translation function
Access via useI18n in any child of StreamVideo:
import { Text } from "react-native";
import { useI18n } from "@stream-io/video-react-native-sdk";
const CustomLabel = () => {
const { t } = useI18n();
return <Text>{t("common.join.label", "Join")}</Text>;
};useI18n() returns { t, tDateTimeParser }, and works outside a provider too, where the default translator renders each call site's inline English.
t() only accepts keys the SDK defines. For a key known only at runtime, wrap it in the exported asDynamicKey() helper and supply its copy through your dictionaries. Prefer a switch over literal t() calls where you can - it keeps the copy statically visible and translatable.
Migrating from v1
| v1 | v2 |
|---|---|
translationsOverrides |
translations - registers over the defaults instead of replacing them |
fallbackLanguage |
removed - use i18nextConfigOverrides: { fallbackLng } |
StreamI18n |
Streami18n (different class, different options) |
StreamI18nProvider |
removed - use StreamVideo |
TranslationsMap |
Record<string, LooseTranslationDictionary> |
TranslationLanguage, TranslatorFunction |
removed |
the translations export / en.json |
removed - English is inline at each call site |
useI18n().i18n |
removed - useI18n() returns { t, tDateTimeParser } |
t("Join") |
t("common.join.label", "Join") |
Every key changed. The old English-sentence key to new namespaced key mapping is published as ai-docs/i18n-2.0-key-map.json in the SDK repository; see also the v1 to v2 migration guide.
An override under an old key fails silently: it never matches, so the SDK's English renders with no error. Type your dictionaries as TranslationDictionary to turn that into a compile error.
Final recommendations
Consult the i18next documentation for: