Localization
Every string the SDK renders goes through a translation function, t(), under a stable key. The composer's placeholder, for example, is the key autoCompleteInput.placeholder, and its English copy, "Send a message", is written next to the key where the component calls t(). A dictionary that maps the key to other text changes what the composer shows. Dates are formatted the same way, through keys whose values are format expressions.
This page covers the Streami18n instance that holds the dictionaries, how keys are named, how to register a language, how TypeScript checks your dictionaries, and how dates are formatted. Translating the messages users send is a separate feature, covered in Message translation.
Streami18n
Streami18n holds the dictionaries and the active language, and formats dates. It wraps i18next for the strings and Day.js for the dates. Its runtime is the @stream-io/i18n package, which the SDK depends on and shares with the React SDK; import everything from stream-chat-react-native (or stream-chat-expo). The SDK ships English only; every other language is a dictionary you register.
Pass one instance to both OverlayProvider and Chat as i18nInstance. OverlayProvider translates what it draws, such as the image gallery, and Chat everything below it. Each one without an instance creates an English one of its own.
The smallest use is re-wording some of the English copy. An instance created with no language is on en, and translationsForLanguage is the dictionary for it:
import type { PropsWithChildren } from "react";
import type { StreamChat } from "stream-chat";
import { Chat, OverlayProvider, Streami18n } from "stream-chat-react-native";
const streami18n = new Streami18n({
translationsForLanguage: {
"autoCompleteInput.placeholder": "Write a reply...",
"indicators.emptyState.noConversations.text": "Nothing here yet",
"messageInput.sendMessage.accessibilityLabel": "Send this message",
},
});
export const App = ({
children,
client,
}: PropsWithChildren<{ client: StreamChat }>) => (
<OverlayProvider i18nInstance={streami18n}>
<Chat client={client} i18nInstance={streami18n}>
{children}
</Chat>
</OverlayProvider>
);Every key you leave out keeps the SDK's copy. Create the instance once, at module scope as here, so the providers don't receive a new one on every render.
Switching the language
Streami18n.setLanguage() switches the active language. It's asynchronous and returns nothing: the new translation function is published to streami18n.state, a state store that OverlayProvider and Chat subscribe to, and the UI re-renders in place. Nothing remounts.
import { useState } from "react";
import { Pressable, Text, View } from "react-native";
import { Streami18n } from "stream-chat-react-native";
export const streami18n = new Streami18n();
const languages = ["en", "de", "it"];
export const LanguagePicker = () => {
const [language, setLanguage] = useState(streami18n.currentLanguage);
const select = async (code: string) => {
await streami18n.setLanguage(code);
// A switch that fails leaves the instance on its previous language.
setLanguage(streami18n.currentLanguage);
};
return (
<View>
{languages.map((code) => (
<Pressable
accessibilityRole="button"
accessibilityState={{ selected: code === language }}
key={code}
onPress={() => select(code)}
>
<Text>{code}</Text>
</Pressable>
))}
</View>
);
};A language with no registered dictionary still works. The UI keeps the SDK's English copy, and dates follow that language if its Day.js locale has been imported. To remember the user's choice, store the code yourself and call Streami18n.setLanguage() when the app starts; the SDK's sample app does this in examples/SampleApp/src/i18n/index.ts.
Using t in your own components
useTranslationContext() returns the active translation function. Components that use it render in the active language and re-render when it changes:
import { Text } from "react-native";
import { useTranslationContext } from "stream-chat-react-native";
export const MemberCount = ({ count }: { count: number }) => {
const { t } = useTranslationContext();
return (
<Text>
{t("channelDetails.memberSection.title", {
count,
defaultValue_one: "{{count}} member",
defaultValue_other: "{{count}} members",
})}
</Text>
);
};| Value | Type | Description |
|---|---|---|
t |
StreamTFunction |
Translates a key into the active language. Typed against the SDK's keys. |
tDateTimeParser |
TDateTimeParser |
Parses a date into a Day.js (or Moment) object in the active language and timezone. |
userLanguage |
string |
The language messages are translated into, from client.user.language. See UI language vs message translation. |
useTranslationContext() throws outside OverlayProvider and Chat. To read an instance's translator outside them, subscribe to its state:
import { Text } from "react-native";
import { Streami18n, useStateStore } from "stream-chat-react-native";
import type { StreamTFunction } from "stream-chat-react-native";
export const streami18n = new Streami18n();
// At module scope: a selector created on every render subscribes again on every render.
const selector = ({ t }: { t: StreamTFunction }) => ({ t });
export const CancelLabel = () => {
const { t } = useStateStore(streami18n.state, selector);
return <Text>{t("common.cancel.label", "Cancel")}</Text>;
};Until the instance is initialized, its t renders the English copy each call passes. OverlayProvider and Chat initialize the instance they receive. For one that neither uses, call Streami18n.init().
Namespaced keys
A key is a dotted identifier. Its first segment is a namespace that matches the component or area rendering it, and the last says how the text is used:
messageInput.sendMessage.accessibilityLabel
^^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
namespace the thing suffixThe suffixes are .label, .text, .accessibilityLabel, .error, .title, .description and .placeholder. They keep one word apart when it plays two roles: messageInput.sendMessage.accessibilityLabel is what a screen reader announces for the send button, and the two can be translated differently from any visible "Send".
Most namespaces are areas of the UI, such as message.*, messageInput.*, messageList.*, channel.*, channelPreview.*, channelDetails.*, poll.* and attachmentPicker.*. Copy shared by several components is under common.*. A few namespaces are not areas:
| Namespace | Contents |
|---|---|
a11y.* |
announcements for screen readers, which never show on screen but still need translating |
notifications.* |
the text of the notifications stream-chat publishes, see Displaying notifications |
language.* |
the names of the languages messages can be translated into ("German", "Japanese"), in English |
relativeTime.* |
the words of short relative dates: "Today", "Yesterday", "3d ago", "2w ago" |
Keys are flat strings that contain dots, not paths into nested objects. A dictionary is a flat object: { "common.cancel.label": "Abbrechen" }, never { common: { cancel: { label: "Abbrechen" } } }.
To find the key behind a string, look in the namespace of the component that renders it, or let your editor autocomplete and show it inside a typed dictionary (see Typed dictionaries).
Plurals
A plural key has an entry per plural form, <key>_one and <key>_other. Components call it by the bare key and pass count, as MemberCount does above. A dictionary supplies the forms its language needs: i18next picks one with Intl.PluralRules, so Russian or Arabic can add _few, _many and _zero. Hermes lacks plural rules for many languages, so the SDK loads the intl-pluralrules polyfill where they're missing.
import { Streami18n } from "stream-chat-react-native";
export const streami18n = new Streami18n();
streami18n.registerTranslation("ru", {
"channelDetails.memberSection.title_one": "{{count}} участник",
"channelDetails.memberSection.title_few": "{{count}} участника",
"channelDetails.memberSection.title_many": "{{count}} участников",
});Keys that are not copy
Eleven keys hold expressions instead of text, so they have no English copy at their call site: ten timestamp.* keys, each a date format run through timestampFormatter, and duration.messageReminder, run through durationFormatter. The SDK ships their values as bundled defaults, and every language is layered over them, so a dictionary inherits them without listing them.
Override one to change how a date or a duration is written: Date and time formatting explains how. Keep these keys out of anything you send to a translation service: an expression with a translated formatter name or option no longer formats the date. Three of them contain words, and those do need translating: see Date and time for a new language.
The SDK also bundles the English of some screen-reader labels whose key reaches t() as a value, such as the labels of the attachment picker's buttons. They're ordinary copy, and you translate them like any other key.
Registering dictionaries
Adding a language takes three things:
- A dictionary of translated keys.
- The Day.js locale, which translates month and weekday names. It's a side-effect import.
- A calendar config, which translates relative dates such as "Yesterday".
Put them in one module per language:
import "dayjs/locale/de";
import type {
DayjsLocaleConfig,
TranslationDictionary,
} from "stream-chat-react-native";
export const deTranslations: TranslationDictionary = {
"autoCompleteInput.placeholder": "Nachricht schreiben",
"channelDetails.memberSection.title_one": "{{count}} Mitglied",
"channelDetails.memberSection.title_other": "{{count}} Mitglieder",
"common.cancel.label": "Abbrechen",
"message.reply.label": "Antworten",
};
export const deDayjsLocaleConfig: DayjsLocaleConfig = {
calendar: {
lastDay: "[Gestern]",
lastWeek: "dddd",
nextDay: "[Morgen]",
nextWeek: "dddd [um] LT",
sameDay: "[Heute]",
sameElse: "L",
},
};A dictionary can be partial. A key it leaves out renders the English copy from its call site, never a raw some.dotted.key, so you can translate ten keys or all of them and ship in between.
Register every language on one instance
Streami18n.registerTranslation(language, dictionary, dayjsLocaleConfig) adds a language to an instance. Register all of them up front, then switch with Streami18n.setLanguage():
import { Streami18n } from "stream-chat-react-native";
import { deDayjsLocaleConfig, deTranslations } from "./de";
import { itDayjsLocaleConfig, itTranslations } from "./it";
export const streami18n = new Streami18n({ language: "de" });
streami18n.registerTranslation("de", deTranslations, deDayjsLocaleConfig);
streami18n.registerTranslation("it", itTranslations, itDayjsLocaleConfig);English needs no dictionary. Registering a dictionary for a language that already has one merges the two, and the later one wins for keys in both. A partial dictionary never removes the bundled formatter keys.
One instance per language isn't needed, and it costs more: each instance is a separate i18next instance, and one swapped into the providers renders English until it has initialized.
For an app with one language, the constructor takes the same three things:
import { Streami18n } from "stream-chat-react-native";
import { deDayjsLocaleConfig, deTranslations } from "./de";
export const streami18n = new Streami18n({
dayjsLocaleConfigForLanguage: deDayjsLocaleConfig,
language: "de",
translationsForLanguage: deTranslations,
});The SDK's sample app has complete German and Italian dictionaries, with their calendar configs, in examples/SampleApp/src/i18n/. Copy one to start a language of your own.
Typed dictionaries
Type a dictionary as TranslationDictionary, and TypeScript checks every key against the SDK's. A mistyped key is a compile error instead of an entry that silently never applies:
import type { TranslationDictionary } from "stream-chat-react-native";
const typo: TranslationDictionary = {
"common.cancel.lable": "Abbrechen",
// TS2353: Object literal may only specify known properties,
// and '"common.cancel.lable"' does not exist in type 'TranslationDictionary'.
};Streami18n.registerTranslation() and translationsForLanguage take this type, so a dictionary written inline is checked without an annotation. A plural key accepts every form Intl.PluralRules can pick (_zero, _one, _two, _few, _many, _other), and a plural form on a key that isn't plural is rejected.
Don't type a dictionary as Partial<Record<TranslationKey, string>>. TranslationKey is the set of keys t() accepts, where a plural is the bare key, so that type rejects the _one and _other entries a dictionary needs.
Reading the catalog from your editor
TranslationCatalog maps every key to its English copy. It's a type only, so it adds nothing to your bundle:
import type { TranslationCatalog } from "stream-chat-react-native";
export type CancelCopy = TranslationCatalog["common.cancel.label"]; // "Cancel"In a typed dictionary, autocomplete lists the keys in a namespace. Hovering a key there shows only string; to read its English copy, look the key up in TranslationCatalog, as above.
Checking a dictionary is complete
TranslationDictionary requires no key. When a new SDK release adds keys, your build stays green, and the new strings render in English until you translate them.
To be told about them, compare your dictionary with the catalog at the type level. Declare the dictionary as const satisfies TranslationDictionary so its literal keys are kept, then exclude them from the catalog's keys:
import type {
TranslationCatalog,
TranslationDictionary,
} from "stream-chat-react-native";
export const deTranslations = {
"common.cancel.label": "Abbrechen",
"message.reply.label": "Antworten",
} as const satisfies TranslationDictionary;
// Formatter keys hold expressions, not copy, so they don't count as untranslated.
type TranslatableKey = Exclude<
keyof TranslationCatalog,
`duration.${string}` | `timestamp.${string}`
>;
// Every key still missing a German translation. Hover it to read the list.
export type Untranslated = Exclude<
TranslatableKey,
keyof typeof deTranslations
>;satisfies still checks every key, so a typo is an error, while as const keeps the literal keys that keyof typeof reads. A plain : TranslationDictionary annotation would widen them to the whole catalog, and Untranslated would always be empty. Add back the three timestamp.* keys that contain words if your language changes them.
Hovering Untranslated lists the missing keys, and the list shrinks as you add them. To make a missing key fail the build, which is what you want in CI for a language that has to stay complete across upgrades, assert that the list is empty:
type AssertNoneMissing<T extends never> = T;
// A compile error naming a missing key, until the dictionary is complete.
export type GermanIsComplete = AssertNoneMissing<Untranslated>;The check runs only at compile time: there is no list of keys to compare against at runtime.
Your own keys, and LooseTranslationDictionary
LooseTranslationDictionary is TranslationDictionary plus any key you like, so one instance can carry your app's copy next to the SDK's. Nothing catches a mistyped SDK key in it, so use it only for the dictionary that needs it, and keep the SDK's keys in a TranslationDictionary:
import { Streami18n } from "stream-chat-react-native";
import type { LooseTranslationDictionary } from "stream-chat-react-native";
export const streami18n = new Streami18n();
const appKeys: LooseTranslationDictionary = {
"myApp.settings.title": "Einstellungen",
};
streami18n.registerTranslation("de", appKeys);t accepts only the SDK's keys, so reading one of your own goes through asDynamicKey:
import { Text } from "react-native";
import { asDynamicKey, useTranslationContext } from "stream-chat-react-native";
export const SettingsTitle = () => {
const { t } = useTranslationContext();
return <Text>{t(asDynamicKey("myApp.settings.title"), "Settings")}</Text>;
};The second argument is the copy that renders when no dictionary has the key. The SDK uses asDynamicKey itself for the keys it only knows at runtime, such as the labels of the channel details rows, which it reads from a table.
Keys that start with timestamp. or duration. are the exception: t accepts any of them without asDynamicKey, because it treats them as formatter expressions. A formatter key of your own has no bundled value, so register its expression for every language, English included.
UI language vs message translation
The SDK works with two languages, and they're independent of each other:
| What it decides | Where it comes from | |
|---|---|---|
| The UI language | the language of the SDK's buttons, labels, placeholders and dates | the Streami18n instance: language, then Streami18n.setLanguage() |
userLanguage |
the language the Stream API translates the text of messages into | client.user.language, set when the user connects |
The UI language is the instance's, and nothing else changes it: no component has a language prop, and the SDK doesn't read the device's language. For a German UI, register a German dictionary and set language: "de". To follow the device, read its locale with a library such as react-native-localize or expo-localization, and pass that.
userLanguage decides which translation of a message the SDK shows: it reads message.i18n.de_text when userLanguage is de, in the message list, in channel-list previews, in channel details' pinned messages, and when the user copies a message. That text exists only when the API has translated the message, for example because automatic translation is turned on for your app. userLanguage follows the connected user, including a language changed after connecting.
Registering a German dictionary doesn't make the API translate messages into German, and connecting a user with language: "de" doesn't translate the UI. Set whichever you need, or both. Message translation explains the message side.
Date and time formatting
Each place the SDK shows a date reads a timestamp.* key, and the reminder header a duration.* key. Their values are format expressions:
| Where | Key | English default |
|---|---|---|
| Date separators in a message list | timestamp.InlineDateSeparator |
"Today", "Yesterday", the weekday within a week, then L |
| The date header at the top of a message list while scrolling | timestamp.StickyHeader |
the same as the separators |
| System messages | timestamp.MessageSystem |
the same as the separators |
| A message's time | timestamp.MessageTimestamp |
LT, such as "3:58 PM" |
| A channel-list row, and a pinned message in channel details | timestamp.ChannelPreviewStatus |
the time today, then "Yesterday", the weekday, then L |
| A thread-list row | timestamp.ThreadListItem |
the same as a channel-list row |
| The image gallery's header | timestamp.ImageGalleryHeader |
the same as the separators |
| The month headings of channel details' files | timestamp.FileAttachmentListSection |
MMMM YYYY, such as "July 2026" |
| A poll vote | timestamp.PollVote |
a relative compact date: "Today", "3d ago" |
| A member's last activity in channel details | timestamp.UserActivityStatus |
"Last seen 3 hours ago" |
| The time left on a message reminder | duration.messageReminder |
"in 10 minutes" |
The separators, the sticky header, system messages and the gallery header set calendar: true with no formats of their own, so they take their wording from the active locale's calendar. For English, the SDK registers the calendar englishCalendarFormats, which it exports so a new language can extend it.
Changing a format
Override a key in a dictionary, and every component that reads it follows. This English instance shows message times on a 24-hour clock, and older date separators with the full date:
import { Streami18n } from "stream-chat-react-native";
export const streami18n = new Streami18n({
translationsForLanguage: {
"timestamp.InlineDateSeparator":
'{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {"lastDay": "[Yesterday]", "lastWeek": "dddd", "nextDay": "[Tomorrow]", "nextWeek": "dddd", "sameDay": "[Today]", "sameElse": "LL"}) }}',
"timestamp.MessageTimestamp":
"{{ timestamp | timestampFormatter(format: HH:mm) }}",
},
});To change a format in one language only, put the key in that language's dictionary. To change it in every language, pass it in runtimeDefaults instead; a language's dictionary still wins over it.
MessageTimestamp also takes a timestampTranslationKey prop, the key it reads its expression from. Set it in a replacement registered through WithComponents to format one place differently from the others that read timestamp.MessageTimestamp, with a key of your own registered for every language.
The formatting syntax
Take this expression:
{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {"lastDay": "[gestern um] LT", "sameElse": "L"}) }}{{and}}mark where a value is inserted.timestampis the value: the date being formatted.|separates the value from the formatter that formats it.timestampFormatteris the formatter. Its options go in the parentheses, separated by;.- An option's value is a word or a number, or a JSON object for
calendarFormats.
timestampFormatter takes these options:
| Option | What it does |
|---|---|
calendar |
Renders the date relative to today ("Yesterday") with Day.js's calendar. |
calendarFormats |
The calendar's six formats: sameDay, nextDay, nextWeek, lastDay, lastWeek and sameElse. Used with calendar. |
format |
A Day.js format, such as HH:mm or dddd L. Ignored when calendar is set. |
relativeCompact |
Renders a short relative date, see Relative compact dates. |
relativeCompactMaxDays |
The most days ago still shown as days, such as "6d ago". Defaults to 6. |
relativeCompactMaxWeeks |
The most weeks ago still shown as weeks, such as "3w ago". Defaults to 3. |
relativeCompactWeekRounding |
floor or ceil: how weeks are counted. Defaults to floor. |
calendarFormats replaces the locale's calendar wording for that key. Give all six formats: a format left out falls back to Day.js's own English one, such as "[Yesterday at] h:mm A". Square brackets keep text literal: [heute um] LT.
durationFormatter takes withSuffix, which turns "10 minutes" into "in 10 minutes" or "10 minutes ago", and format, a Day.js duration format. fromNowFormatter renders a date as "3 hours ago", and takes withoutSuffix, which drops the "ago". timestamp.UserActivityStatus uses it.
These expressions follow i18next's formatting rules.
Relative compact dates
relativeCompact: true renders a short label instead of a date. timestamp.PollVote uses it.
| Age | Renders |
|---|---|
| today | relativeTime.today: "Today" |
| yesterday | relativeTime.yesterday: "Yesterday" |
up to relativeCompactMaxDays days |
relativeTime.daysAgo: "3d ago" |
within relativeCompactMaxWeeks weeks |
relativeTime.weeksAgo: "2w ago" |
| older, or in the future | the date as DD/MM/YY |
The words are ordinary copy: translate them with the relativeTime.* keys. relativeTime.daysAgo and relativeTime.weeksAgo are plural keys, so they take _one, _other and any other form your language needs.
relativeCompactWeekRounding decides how weeks are counted, and when the label gives way to the date:
| Value | Weeks counted | Shown as weeks until | 8 days | 27 days |
|---|---|---|---|---|
floor (default) |
whole weeks | relativeCompactMaxWeeks weeks |
"1w ago" | "3w ago" |
ceil |
rounded up | relativeCompactMaxWeeks * 7 days |
"2w ago" | DD/MM/YY |
Date and time for a new language
Two things make dates read in another language, and they're separate.
Month and weekday names come from the Day.js locale. Only en is bundled, so import the locale you need, as import "dayjs/locale/de". Day.js publishes locales for many languages. Without the import, L and dddd render English names whatever your dictionary says.
The import has to reach the same copy of Day.js the SDK uses: a locale registers on one copy only. The SDK depends on dayjs ^1.11.23. Leave dayjs out of your app's dependencies, or declare a range that resolves to the same version, so your package manager installs one copy. If your app has its own copy anyway, pass it as DateTimeParser, as shown in Timezones and other parsers.
Relative wording ("Gestern") comes from the calendar config: dayjsLocaleConfigForLanguage on the constructor, or the third argument of Streami18n.registerTranslation(). No Day.js locale file includes one, so a new language needs both. Without it, a relative date mixes Day.js's English with translated names, as in "Last Mittwoch at 5:10 PM". The config's type is DayjsLocaleConfig, a Day.js locale config plus calendar. Give all six calendar formats, as in the German module in Registering dictionaries. The config is merged into the locale, so a config with only calendar keeps the rest of the imported locale.
The calendar config reaches the keys that set calendar: true with no formats of their own. Three keys contain English words that it doesn't reach:
| Key | English in it |
|---|---|
timestamp.ChannelPreviewStatus |
Yesterday, Tomorrow and at, in its calendarFormats |
timestamp.ThreadListItem |
Yesterday, Tomorrow and at, in its calendarFormats |
timestamp.UserActivityStatus |
Last seen, around the formatter |
Translate them by overriding the keys in the language's dictionary. Change only the bracketed words and the text outside the {{ }}:
import type { TranslationDictionary } from "stream-chat-react-native";
export const deDateKeys: TranslationDictionary = {
"timestamp.ChannelPreviewStatus":
'{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {"lastDay":"[Gestern]", "lastWeek":"dddd", "nextDay":"[Morgen]", "nextWeek":"dddd [um] LT", "sameDay":"LT", "sameElse":"L"}) }}',
"timestamp.ThreadListItem":
'{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {"lastDay":"[Gestern]", "lastWeek":"dddd", "nextDay":"[Morgen]", "nextWeek":"dddd [um] LT", "sameDay":"LT", "sameElse":"L"}) }}',
"timestamp.UserActivityStatus":
"Zuletzt gesehen {{ timestamp | fromNowFormatter }}",
};Timezones and other parsers
Dates render in the device's timezone. To use another one, pass a timezone identifier as timezone:
import { Streami18n } from "stream-chat-react-native";
export const streami18n = new Streami18n({ timezone: "Europe/Prague" });To keep dates in English whatever the UI language, set disableDateTimeTranslations: true.
DateTimeParser replaces the Day.js the SDK uses. Pass your app's own dayjs when it's a different copy from the SDK's, so the locales your app imports and the parser are the same module. The SDK registers the plugins it needs on it:
import dayjs from "dayjs";
import "dayjs/locale/de";
import { Streami18n } from "stream-chat-react-native";
export const streami18n = new Streami18n({
DateTimeParser: dayjs,
language: "de",
});Moment works as a parser too. Use moment-timezone for timezone to apply. A calendar config can't be applied to Moment, so set the locale up on Moment itself.
Custom formatters
formatters replaces a built-in formatter or adds one. A formatter is a function that receives the instance's context and returns the function that formats a value. This one formats every timestampFormatter expression with Intl.DateTimeFormat:
import { Streami18n } from "stream-chat-react-native";
export const streami18n = new Streami18n({
formatters: {
timestampFormatter:
({ currentLanguage }) =>
(value) =>
new Intl.DateTimeFormat(currentLanguage, {
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(value)),
},
});Replacing timestampFormatter replaces it for every timestamp.* key, and the options in their expressions reach your function as its third argument. To change some keys only, add a formatter under a new name and use that name in those keys' expressions. On Hermes, check that Intl.DateTimeFormat supports the options you use on every platform you ship.
Streami18n reference
Constructor options
| Option | Type | Default | Description |
|---|---|---|---|
language |
string |
'en' |
The language to render. |
translationsForLanguage |
TranslationDictionary |
The dictionary for language. |
|
dayjsLocaleConfigForLanguage |
DayjsLocaleConfig |
The Day.js locale config and calendar wording for language. |
|
DateTimeParser |
Day.js or Moment | Day.js | The date library. Day.js gets the plugins the formatters need. |
timezone |
string |
the device's | The timezone dates render in. |
disableDateTimeTranslations |
boolean |
false |
Keeps dates in English whatever the language. |
formatters |
object |
the built-in three | Replaces or adds formatters. See Custom formatters. |
runtimeDefaults |
Record<string, string> |
Keys added to every language, merged over the SDK's bundled defaults. | |
parseMissingKeyHandler |
function |
see below | Called for a key with no translation, no bundled value and no default. |
i18nextConfigOverrides |
Partial<InitOptions> |
Any i18next init option, applied over the SDK's. Leave keySeparator and nsSeparator as they are. |
|
logger |
function |
console.warn |
Receives the instance's warnings. |
debug |
boolean |
false |
Turns on i18next's debug output. |
parseMissingKeyHandler never sees untranslated copy: a key with no translation renders its English default without calling it. It's called only for a key with nothing to render at all, for example one passed through asDynamicKey with no default. The default handler logs a warning and returns the key.
import { Streami18n } from "stream-chat-react-native";
export const streami18n = new Streami18n({
parseMissingKeyHandler: (key) => {
console.error(`Nothing to render for the translation key ${key}`);
return "";
},
});To find copy that isn't translated yet, use the compile-time check.
Methods
| Method | Description |
|---|---|
registerTranslation(language, dictionary, dayjsLocaleConfig?) |
Adds or extends a language. Merged over what the language already has. |
setLanguage(language) |
Switches the active language. Returns a promise of nothing; the new translator is published to state. |
init() |
Initializes i18next. The providers call it. Calling it again returns the same promise. |
overrideTFunction(t) |
Replaces the translation function, for an app that already has its own i18n layer. |
Properties
| Property | Type | Description |
|---|---|---|
currentLanguage |
string |
The active language. |
registeredLanguages |
ReadonlySet<string> |
The languages a dictionary was registered for, plus en. |
initialized |
boolean |
Whether init() has finished. Until then, t returns each key's English copy. |
t |
StreamTFunction |
The current translation function. |
tDateTimeParser |
TDateTimeParser |
The current date parser. |
state |
StateStore |
{ t, tDateTimeParser, language, initialized }. Subscribe to it to follow language changes. |
i18nInstance |
i18n |
The underlying i18next instance. |
Used in
- Message translation, for the text of messages.
- Displaying notifications, for notification copy.
- Upgrade to v10, which maps the v9 keys to these.
Pitfalls
- An instance on
Chatonly.OverlayProviderthen uses an English instance of its own, and the image gallery stays in English. Pass the same instance to both. - A dictionary and no dates. A dictionary translates the copy. Month names need the Day.js locale import, and relative dates need the calendar config. Translating one and forgetting the others is the most common gap.
- Two copies of Day.js. A locale imported into your app's copy never reaches the SDK's, and dates stay English with no error. Keep one copy, or pass yours as
DateTimeParser. - Untyped dictionaries. A key from an older version or a typo never matches, and nothing reports it. Type dictionaries as
TranslationDictionary. - Setting the language through the user.
languageinconnectUsersets the language messages are translated into, not the UI's. Set the UI language on theStreami18ninstance. - Sending formatter keys to translators. The
timestamp.*andduration.*values are expressions. Translate only the words in the three keys that have them; a translated formatter name or option stops formatting. - A new instance on every render.
new Streami18n()inside a component gives the providers a new instance each time, which initializes again. Create it at module scope. - New copy after an upgrade. New keys render in English without a warning. For a language that has to stay complete, run the completeness check in CI.