# Localization

> **Info:** This is release candidate documentation for **Stream Chat React Native SDK v10**. For the latest stable version, see the [latest version (v9)](https://getstream.io/chat/docs/sdk/react-native/).

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](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/message-translation/).

## Streami18n

`Streami18n` holds the dictionaries and the active language, and formats dates. It wraps [`i18next`](https://www.i18next.com/) for the strings and [Day.js](https://day.js.org/) 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:

```tsx
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.

```tsx
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:

```tsx
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](#ui-language-vs-message-translation). |

`useTranslationContext()` throws outside `OverlayProvider` and `Chat`. To read an instance's translator outside them, subscribe to its state:

```tsx
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:

```text
messageInput.sendMessage.accessibilityLabel
^^^^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
namespace    the thing   suffix
```

The 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](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/displaying-notifications/#translating-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](#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.

```ts
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](#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](#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:

1. **A dictionary** of translated keys.
2. **The Day.js locale,** which translates month and weekday names. It's a side-effect import.
3. **A calendar config,** which translates relative dates such as "Yesterday".

Put them in one module per language:

```ts title="src/i18n/de.ts"
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()`:

```ts title="src/i18n/index.ts"
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:

```ts
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:

```ts
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.

>
> **Warning:** 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:

```ts
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:

```ts title="src/i18n/de.ts"
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:

```ts
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`:

```ts
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`:

```tsx
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](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/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](#relative-compact-dates): "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:

```ts
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:

```text
{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {"lastDay": "[gestern um] LT", "sameElse": "L"}) }}
```

- `{{` and `}}` mark where a value is inserted.
- `timestamp` is the value: the date being formatted.
- `|` separates the value from the formatter that formats it.
- `timestampFormatter` is 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](https://day.js.org/docs/en/plugin/calendar).     |
| `calendarFormats`             | The calendar's six formats: `sameDay`, `nextDay`, `nextWeek`, `lastDay`, `lastWeek` and `sameElse`. Used with `calendar`.  |
| `format`                      | A [Day.js format](https://day.js.org/docs/en/display/format), such as `HH:mm` or `dddd L`. Ignored when `calendar` is set. |
| `relativeCompact`             | Renders a short relative date, see [Relative compact dates](#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](https://day.js.org/docs/en/durations/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](https://www.i18next.com/translation-function/formatting#basic-usage).

### 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](https://github.com/iamkun/dayjs/tree/dev/src/locale). 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](#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](#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 `{{ }}`:

```ts
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](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) as `timezone`:

```ts
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:

```ts
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](https://momentjs.com/) 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`:

```ts
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](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) 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](#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.

```ts
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](#checking-a-dictionary-is-complete).

### 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](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/message-translation/), for the text of messages.
- [Displaying notifications](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/displaying-notifications/#translating-notifications), for notification copy.
- [Upgrade to v10](https://getstream.io/chat/docs/sdk/react-native/v10/release-guides/upgrade-to-v10/), which maps the v9 keys to these.

## Pitfalls

- **An instance on `Chat` only.** `OverlayProvider` then 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.** `language` in `connectUser` sets the language messages are translated into, not the UI's. Set the UI language on the `Streami18n` instance.
- **Sending formatter keys to translators.** The `timestamp.*` and `duration.*` 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](#checking-a-dictionary-is-complete) in CI.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/localization/](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/localization/).