diff --git a/docusaurus/docs/reactnative/guides/date-time-formatting.mdx b/docusaurus/docs/reactnative/guides/date-time-formatting.mdx
new file mode 100644
index 0000000000..073c1161f8
--- /dev/null
+++ b/docusaurus/docs/reactnative/guides/date-time-formatting.mdx
@@ -0,0 +1,129 @@
+---
+id: date-time-formatting
+title: Date and time formatting
+---
+
+In this guide we will learn how date a time formatting can be customized within SDK's components.
+
+## SDK components displaying date & time
+
+The following components provided by the SDK display datetime:
+
+- `ChannelPreviewStatus` - Component showing last message date and time in `ChannelList`.
+- `ImageGalleryHeader` - Component showing the header in the `ImageGallery`.
+- `InlineDateSeparator` - Component separating groups of messages in `MessageList`.
+- `MessageEditedTimestamp` - Component showing edited message time when clicked on an edited message.
+- `MessageSystem` - Component showing system message.
+- `MessageTimestamp` - Component showing message timestamp.
+- `StickyHeader` - Component showing sticky header on the top of the `MessageList`/`Channel`.
+
+## Format Customization
+
+The datetime format can be customized by providing date format through the `i18n` JSON.
+
+### Date & time formatting with i18n service
+
+Formatting via i18n service allows for SDK wide configuration. The configuration is stored with other translations in JSON files. Formatting with i18n service has the following advantages:
+
+- It is centralized.
+- It takes into consideration the locale out of the box.
+- Allows for high granularity - formatting per string, not component (opposed to props approach).
+- Allows for high re-usability - apply the same configuration in multiple places via the same translation key.
+- Allows for custom formatting logic.
+
+The default datetime formatting configuration is stored in the JSON translation files. The default translation keys are namespaced with prefix `timestamp/` followed by the component name. For example, the message date formatting can be targeted via `timestamp/MessageTimestamp`, because the underlying component is called `MessageTimestamp`.
+
+We can apply custom configuration in all the translation JSON files. It could look similar to the following example key-value pair.
+
+```json
+"timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: YYYY) }}",
+```
+
+Besides overriding the formatting parameters above, we can customize the translation key via `timestampTranslationKey` prop. All the above components (`ChannelPreviewStatus`, `ImageGalleryHeader`, `InlineDateSeparator`, `MessageEditedTimestamp`, `MessageSystem`, `MessageTimestamp`, `StickyHeader`) accept this prop.
+
+```tsx
+import { MessageTimestampProps, MessageTimestamp } from 'stream-chat-react-native';
+
+const CustomMessageTimestamp = (props: MessageTimestampProps) => (
+
+);
+```
+
+### Understanding the formatting syntax
+
+Once the default prop values are nullified, we override the default formatting rules in the JSON translation value. We can take a look at an example:
+
+```json
+"timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}",
+```
+
+or with custom calendar formats:
+
+```json
+"timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay: \"[gestern um] LT\", \"lastWeek\": \"[letzten] dddd [um] LT\", \"nextDay\": \"[morgen um] LT\", \"nextWeek\": \"dddd [um] LT\", \"sameDay\": \"[heute um] LT\", \"sameElse\": \"L\"}) }}",
+```
+
+or with custom format:
+
+```json
+"timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}",
+```
+
+Let's dissect the example:
+
+- The curly brackets (`{{`, `}}`) indicate the place where a value will be interpolated (inserted) into the string.
+- Variable `timestamp` is the name of variable which value will be inserted into the string.
+- The `|` character is a pipe that separates the variable from the formatting function.
+- The `timestampFormatter` is the name of the formatting function that is used to convert the `timestamp` value into desired format
+- The `timestampFormatter` can be passed the same parameters as the React components (`calendar`, `calendarFormats`, `format`) as if the function was called with these values.
+
+**Params**:
+
+- `calendar` - This is a boolean field to decide if the date format should be in calendar format or not. The default value is `false`.
+- `calendarFormats` - This is an object that contains the formats for the calendar. The default value is `{ sameDay: 'LT', nextDay: 'LT', nextWeek: 'dddd', lastDay: 'dddd', lastWeek: 'dddd', sameElse: 'L' }`.
+- `format` - This is a string that contains the format of the date.
+
+If calendar formatting is enabled, the dates are formatted with time-relative words ("yesterday at ...", "last ..."). The calendar strings can be further customized with `calendarFormats` object. The `calendarFormats` object has to cover all the formatting cases as shows the example below:
+
+```js
+{
+ lastDay: '[gestern um] LT',
+ lastWeek: '[letzten] dddd [um] LT',
+ nextDay: '[morgen um] LT',
+ nextWeek: 'dddd [um] LT',
+ sameDay: '[heute um] LT',
+ sameElse: 'L',
+}
+```
+
+:::important
+If any of the `calendarFormats` keys are missing, then the underlying library will fall back to hard-coded english equivalents
+:::
+
+If `calendar` formatting is enabled, the `format` prop would be ignored. So to apply the `format` string, the `calendar` has to be disabled.
+
+:::note
+The described rules follow the formatting rules required by the i18n library used under the hood - `i18next`. You can learn more about the rules in [the formatting section of the `i18next` documentation](https://www.i18next.com/translation-function/formatting#basic-usage).
+:::
+
+### Custom datetime formatter functions
+
+Besides overriding the configuration parameters, we can override the default `timestampFormatter` function by providing custom `Streami18n` instance:
+
+```tsx
+import { Chat, Streami18n } from 'stream-chat-react-native';
+
+const chatClient = 'Your Chat client here';
+
+const i18n = new Streami18n({
+ formatters: {
+ timestampFormatter: () => (val: string | Date) => {
+ return new Date(val).getTime() + '';
+ },
+ },
+});
+
+export const ChatApp = ({ apiKey, userId, userToken }) => {
+ return ;
+};
+```
diff --git a/docusaurus/docs/reactnative/ui-components/message-edited-timestamp.mdx b/docusaurus/docs/reactnative/ui-components/message-edited-timestamp.mdx
index 904b7185c7..c09292a6f7 100644
--- a/docusaurus/docs/reactnative/ui-components/message-edited-timestamp.mdx
+++ b/docusaurus/docs/reactnative/ui-components/message-edited-timestamp.mdx
@@ -11,38 +11,16 @@ This is the default component provided to the prop [`MessageEditedTimestamp`](..
## Props
-###
_overrides the value from [MessageContext](../../contexts/message-context#message)_
`message` {#message}
+### _overrides the value from [MessageContext](../../contexts/message-context#message)_
`message` `{#message}`
-### `calendar`
+## UI components
-Whether to show the time in Calendar time format. Calendar time displays time relative to a today's date.
+### `MessageTimestamp`
-| Type | Default |
-| ---------------------- | ----------- |
-| `Boolean`\|`undefined` | `undefined` |
-
-### `format`
-
-Format of the date.
-
-| Type | Default |
-| --------------------- | ----------- |
-| `String`\|`undefined` | `undefined` |
-
-### `formatDate`
-
-Function to format the date.
-
-| Type | Default |
-| ----------------------- | ----------- |
-| `Function`\|`undefined` | `undefined` |
-
-### `timestamp`
-
-The date to be shown after formatting.
+The Component that renders the message timestamp.
| Type | Default |
| ----------------------------- | ----------- |
-| `String`\|`Date`\|`undefined` | `undefined` |
+| `ComponentType` \|`undefined` | `undefined` |
diff --git a/docusaurus/docs/reactnative/ui-components/message-footer.mdx b/docusaurus/docs/reactnative/ui-components/message-footer.mdx
index d43a276d27..45ada17548 100644
--- a/docusaurus/docs/reactnative/ui-components/message-footer.mdx
+++ b/docusaurus/docs/reactnative/ui-components/message-footer.mdx
@@ -77,3 +77,13 @@ Weather message is deleted or not. In case of deleted message, `'Only visible to
### _overrides the value from [MessageContext](../../contexts/message-context#showmessagestatus)_
`showMessageStatus` {#showmessagestatus}
+
+## UI Components
+
+### `MessageTimestamp`
+
+The Component that renders the message timestamp.
+
+| Type | Default |
+| ----------------------------- | ----------- |
+| `ComponentType` \|`undefined` | `undefined` |
diff --git a/docusaurus/sidebars-react-native.json b/docusaurus/sidebars-react-native.json
index b4879ebd97..fc4f57a716 100644
--- a/docusaurus/sidebars-react-native.json
+++ b/docusaurus/sidebars-react-native.json
@@ -129,6 +129,7 @@
],
"Advanced Guides": [
"guides/audio-messages-support",
+ "guides/date-time-formatting",
"customization/typescript",
"basics/troubleshooting",
"basics/stream_chat_with_navigation",
diff --git a/package/package.json b/package/package.json
index f2ed02acb4..4194a051a8 100644
--- a/package/package.json
+++ b/package/package.json
@@ -70,7 +70,8 @@
"@gorhom/bottom-sheet": "4.4.8",
"dayjs": "1.10.5",
"emoji-regex": "^10.3.0",
- "i18next": "20.2.4",
+ "i18next": "^21.6.14",
+ "intl-pluralrules": "^2.0.1",
"linkifyjs": "^4.1.1",
"lodash-es": "4.17.21",
"mime-types": "^2.1.34",
diff --git a/package/src/components/Attachment/__tests__/Giphy.test.js b/package/src/components/Attachment/__tests__/Giphy.test.js
index 1b7681af4f..df2060d89f 100644
--- a/package/src/components/Attachment/__tests__/Giphy.test.js
+++ b/package/src/components/Attachment/__tests__/Giphy.test.js
@@ -23,7 +23,7 @@ import { generateMember } from '../../../mock-builders/generator/member';
import { generateMessage } from '../../../mock-builders/generator/message';
import { generateUser } from '../../../mock-builders/generator/user';
import { getTestClientWithUser } from '../../../mock-builders/mock';
-import { Streami18n } from '../../../utils/Streami18n';
+import { Streami18n } from '../../../utils/i18n/Streami18n';
import { ImageLoadingFailedIndicator } from '../../Attachment/ImageLoadingFailedIndicator';
import { ImageLoadingIndicator } from '../../Attachment/ImageLoadingIndicator';
import { Channel } from '../../Channel/Channel';
diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx
index 25a38793ea..b42bfe0c50 100644
--- a/package/src/components/Channel/Channel.tsx
+++ b/package/src/components/Channel/Channel.tsx
@@ -127,6 +127,7 @@ import { MessageReplies as MessageRepliesDefault } from '../Message/MessageSimpl
import { MessageRepliesAvatars as MessageRepliesAvatarsDefault } from '../Message/MessageSimple/MessageRepliesAvatars';
import { MessageSimple as MessageSimpleDefault } from '../Message/MessageSimple/MessageSimple';
import { MessageStatus as MessageStatusDefault } from '../Message/MessageSimple/MessageStatus';
+import { MessageTimestamp as MessageTimestampDefault } from '../Message/MessageSimple/MessageTimestamp';
import { ReactionList as ReactionListDefault } from '../Message/MessageSimple/ReactionList';
import { AttachButton as AttachButtonDefault } from '../MessageInput/AttachButton';
import { CommandsButton as CommandsButtonDefault } from '../MessageInput/CommandsButton';
@@ -156,6 +157,7 @@ import { MessageList as MessageListDefault } from '../MessageList/MessageList';
import { MessageSystem as MessageSystemDefault } from '../MessageList/MessageSystem';
import { NetworkDownIndicator as NetworkDownIndicatorDefault } from '../MessageList/NetworkDownIndicator';
import { ScrollToBottomButton as ScrollToBottomButtonDefault } from '../MessageList/ScrollToBottomButton';
+import { StickyHeader as StickyHeaderDefault } from '../MessageList/StickyHeader';
import { TypingIndicator as TypingIndicatorDefault } from '../MessageList/TypingIndicator';
import { TypingIndicatorContainer as TypingIndicatorContainerDefault } from '../MessageList/TypingIndicatorContainer';
import { OverlayReactionList as OverlayReactionListDefault } from '../MessageOverlay/OverlayReactionList';
@@ -305,6 +307,7 @@ export type ChannelPropsWithContext<
| 'MessageStatus'
| 'MessageSystem'
| 'MessageText'
+ | 'MessageTimestamp'
| 'myMessageTheme'
| 'onLongPressMessage'
| 'onPressInMessage'
@@ -542,6 +545,7 @@ const ChannelWithContext = <
MessageStatus = MessageStatusDefault,
MessageSystem = MessageSystemDefault,
MessageText,
+ MessageTimestamp = MessageTimestampDefault,
MoreOptionsButton = MoreOptionsButtonDefault,
myMessageTheme,
NetworkDownIndicator = NetworkDownIndicatorDefault,
@@ -573,7 +577,7 @@ const ChannelWithContext = <
ShowThreadMessageInChannelButton = ShowThreadMessageInChannelButtonDefault,
StartAudioRecordingButton = AudioRecordingButtonDefault,
stateUpdateThrottleInterval = defaultThrottleInterval,
- StickyHeader,
+ StickyHeader = StickyHeaderDefault,
supportedReactions = reactionData,
t,
thread: threadProps,
@@ -2328,6 +2332,7 @@ const ChannelWithContext = <
MessageStatus,
MessageSystem,
MessageText,
+ MessageTimestamp,
myMessageTheme,
onLongPressMessage,
onPressInMessage,
diff --git a/package/src/components/Channel/hooks/useCreateMessagesContext.ts b/package/src/components/Channel/hooks/useCreateMessagesContext.ts
index 87c33be1e1..caeab52edc 100644
--- a/package/src/components/Channel/hooks/useCreateMessagesContext.ts
+++ b/package/src/components/Channel/hooks/useCreateMessagesContext.ts
@@ -69,6 +69,7 @@ export const useCreateMessagesContext = <
MessageStatus,
MessageSystem,
MessageText,
+ MessageTimestamp,
myMessageTheme,
onLongPressMessage,
onPressInMessage,
@@ -165,6 +166,7 @@ export const useCreateMessagesContext = <
MessageStatus,
MessageSystem,
MessageText,
+ MessageTimestamp,
myMessageTheme,
onLongPressMessage,
onPressInMessage,
diff --git a/package/src/components/ChannelPreview/ChannelPreviewStatus.tsx b/package/src/components/ChannelPreview/ChannelPreviewStatus.tsx
index ec0a4142ed..21b904ae2c 100644
--- a/package/src/components/ChannelPreview/ChannelPreviewStatus.tsx
+++ b/package/src/components/ChannelPreview/ChannelPreviewStatus.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { ChannelPreviewProps } from './ChannelPreview';
@@ -6,9 +6,11 @@ import type { ChannelPreviewMessengerPropsWithContext } from './ChannelPreviewMe
import { MessageReadStatus } from './hooks/useLatestMessagePreview';
import { useTheme } from '../../contexts/themeContext/ThemeContext';
+import { useTranslationContext } from '../../contexts/translationContext/TranslationContext';
import { Check, CheckAll } from '../../icons';
import type { DefaultStreamChatGenerics } from '../../types/types';
+import { getDateString } from '../../utils/i18n/getDateString';
const styles = StyleSheet.create({
date: {
@@ -35,6 +37,7 @@ export const ChannelPreviewStatus = <
props: ChannelPreviewStatusProps,
) => {
const { formatLatestMessageDate, latestMessagePreview } = props;
+ const { t, tDateTimeParser } = useTranslationContext();
const {
theme: {
channelPreview: { checkAllIcon, checkIcon, date },
@@ -44,6 +47,17 @@ export const ChannelPreviewStatus = <
const created_at = latestMessagePreview.messageObject?.created_at;
const latestMessageDate = created_at ? new Date(created_at) : new Date();
+
+ const formattedDate = useMemo(
+ () =>
+ getDateString({
+ date: created_at,
+ t,
+ tDateTimeParser,
+ timestampTranslationKey: 'timestamp/ChannelPreviewStatus',
+ }),
+ [created_at, t, tDateTimeParser],
+ );
const status = latestMessagePreview.status;
return (
@@ -56,7 +70,7 @@ export const ChannelPreviewStatus = <
{formatLatestMessageDate && latestMessageDate
? formatLatestMessageDate(latestMessageDate).toString()
- : latestMessagePreview.created_at.toString()}
+ : formattedDate}
);
diff --git a/package/src/components/ChannelPreview/hooks/useLatestMessagePreview.ts b/package/src/components/ChannelPreview/hooks/useLatestMessagePreview.ts
index 0238cf9f5f..29c69a5622 100644
--- a/package/src/components/ChannelPreview/hooks/useLatestMessagePreview.ts
+++ b/package/src/components/ChannelPreview/hooks/useLatestMessagePreview.ts
@@ -1,13 +1,10 @@
import { useEffect, useState } from 'react';
+import { TFunction } from 'i18next';
import type { Channel, ChannelState, MessageResponse, StreamChat, UserResponse } from 'stream-chat';
import { useChatContext } from '../../../contexts/chatContext/ChatContext';
-import {
- isDayOrMoment,
- TDateTimeParser,
- useTranslationContext,
-} from '../../../contexts/translationContext/TranslationContext';
+import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
import { useTranslatedMessage } from '../../../hooks/useTranslatedMessage';
import type { DefaultStreamChatGenerics } from '../../../types/types';
@@ -21,13 +18,13 @@ type LatestMessage<
export type LatestMessagePreview<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
> = {
- created_at: string | number | Date;
messageObject: LatestMessage | undefined;
previews: {
bold: boolean;
text: string;
}[];
status: number;
+ created_at?: string | Date;
};
const getMessageSenderName = <
@@ -131,22 +128,6 @@ const getLatestMessageDisplayText = <
];
};
-const getLatestMessageDisplayDate = <
- StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
->(
- message: LatestMessage | undefined,
- tDateTimeParser: TDateTimeParser,
-) => {
- const parserOutput = tDateTimeParser(message?.created_at);
- if (isDayOrMoment(parserOutput)) {
- if (parserOutput.isSame(new Date(), 'day')) {
- return parserOutput.format('LT');
- }
- return parserOutput.format('L');
- }
- return parserOutput;
-};
-
export enum MessageReadStatus {
NOT_SENT_BY_CURRENT_USER = 0,
UNREAD = 1,
@@ -190,13 +171,12 @@ const getLatestMessagePreview = <
channel: Channel;
client: StreamChat;
readEvents: boolean;
- t: (key: string) => string;
- tDateTimeParser: TDateTimeParser;
+ t: TFunction;
lastMessage?:
| ReturnType['formatMessage']>
| MessageResponse;
}) => {
- const { channel, client, lastMessage, readEvents, t, tDateTimeParser } = params;
+ const { channel, client, lastMessage, readEvents, t } = params;
const messages = channel.state.messages;
@@ -219,7 +199,7 @@ const getLatestMessagePreview = <
const message = lastMessage !== undefined ? lastMessage : channelStateLastMessage;
return {
- created_at: getLatestMessageDisplayDate(message, tDateTimeParser),
+ created_at: message?.created_at,
messageObject: message,
previews: getLatestMessageDisplayText(channel, client, message, t),
status: getLatestMessageReadStatus(channel, client, message, readEvents),
@@ -240,7 +220,7 @@ export const useLatestMessagePreview = <
forceUpdate: number,
) => {
const { client } = useChatContext();
- const { t, tDateTimeParser } = useTranslationContext();
+ const { t } = useTranslationContext();
const channelConfigExists = typeof channel?.getConfig === 'function';
@@ -286,7 +266,6 @@ export const useLatestMessagePreview = <
lastMessage: translatedLastMessage,
readEvents,
t,
- tDateTimeParser,
}),
),
[channelLastMessageString, forceUpdate, readEvents, readStatus],
diff --git a/package/src/components/Chat/Chat.tsx b/package/src/components/Chat/Chat.tsx
index 8f35376dee..017bce93f0 100644
--- a/package/src/components/Chat/Chat.tsx
+++ b/package/src/components/Chat/Chat.tsx
@@ -27,8 +27,8 @@ import { SDK } from '../../native';
import { QuickSqliteClient } from '../../store/QuickSqliteClient';
import type { DefaultStreamChatGenerics } from '../../types/types';
import { DBSyncManager } from '../../utils/DBSyncManager';
+import type { Streami18n } from '../../utils/i18n/Streami18n';
import { StreamChatRN } from '../../utils/StreamChatRN';
-import type { Streami18n } from '../../utils/Streami18n';
import { version } from '../../version.json';
init();
diff --git a/package/src/components/Chat/__tests__/Chat.test.js b/package/src/components/Chat/__tests__/Chat.test.js
index 2df56e57f4..07b7850f75 100644
--- a/package/src/components/Chat/__tests__/Chat.test.js
+++ b/package/src/components/Chat/__tests__/Chat.test.js
@@ -10,7 +10,7 @@ import { useTranslationContext } from '../../../contexts/translationContext/Tran
import dispatchConnectionChangedEvent from '../../../mock-builders/event/connectionChanged';
import dispatchConnectionRecoveredEvent from '../../../mock-builders/event/connectionRecovered';
import { getTestClient } from '../../../mock-builders/mock';
-import { Streami18n } from '../../../utils/Streami18n';
+import { Streami18n } from '../../../utils/i18n/Streami18n';
import { Chat } from '../Chat';
const ChatContextConsumer = ({ fn }) => {
diff --git a/package/src/components/ImageGallery/components/ImageGalleryHeader.tsx b/package/src/components/ImageGallery/components/ImageGalleryHeader.tsx
index 834eb6ba61..f83b1eacb8 100644
--- a/package/src/components/ImageGallery/components/ImageGalleryHeader.tsx
+++ b/package/src/components/ImageGallery/components/ImageGalleryHeader.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useMemo, useState } from 'react';
import { Pressable, SafeAreaView, StyleSheet, Text, View, ViewStyle } from 'react-native';
import Animated, { Extrapolate, interpolate, useAnimatedStyle } from 'react-native-reanimated';
@@ -8,7 +8,7 @@ import { useTranslationContext } from '../../../contexts/translationContext/Tran
import { Close } from '../../../icons';
import type { DefaultStreamChatGenerics } from '../../../types/types';
-import { getDateString } from '../../../utils/getDateString';
+import { getDateString } from '../../../utils/i18n/getDateString';
import type { Photo } from '../ImageGallery';
const ReanimatedSafeAreaView = Animated.createAnimatedComponent
@@ -70,6 +70,7 @@ type Props;
visible: Animated.SharedValue;
photo?: Photo;
+ /* Lookup key in the language corresponding translations sheet to perform date formatting */
};
export const ImageGalleryHeader = <
@@ -98,7 +99,16 @@ export const ImageGalleryHeader = <
const { t, tDateTimeParser } = useTranslationContext();
const { setOverlay } = useOverlayContext();
- const date = getDateString({ calendar: true, date: photo?.created_at, tDateTimeParser });
+ const date = useMemo(
+ () =>
+ getDateString({
+ date: photo?.created_at,
+ t,
+ tDateTimeParser,
+ timestampTranslationKey: 'timestamp/ImageGalleryHeader',
+ }),
+ [photo?.created_at, t, tDateTimeParser],
+ );
const headerStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
diff --git a/package/src/components/Message/MessageSimple/MessageEditedTimestamp.tsx b/package/src/components/Message/MessageSimple/MessageEditedTimestamp.tsx
index 7db27bf123..2ce2963a96 100644
--- a/package/src/components/Message/MessageSimple/MessageEditedTimestamp.tsx
+++ b/package/src/components/Message/MessageSimple/MessageEditedTimestamp.tsx
@@ -1,12 +1,11 @@
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
-import { MessageTimestamp, MessageTimestampProps } from './MessageTimestamp';
-
import {
MessageContextValue,
useMessageContext,
} from '../../../contexts/messageContext/MessageContext';
+import { MessagesContextValue } from '../../../contexts/messagesContext/MessagesContext';
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
import { DefaultStreamChatGenerics } from '../../../types/types';
@@ -14,14 +13,15 @@ import { isEditedMessage } from '../../../utils/utils';
export type MessageEditedTimestampProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
-> = Partial, 'message'>> & MessageTimestampProps;
+> = Partial, 'message'>> &
+ Partial, 'MessageTimestamp'>>;
export const MessageEditedTimestamp = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
>(
props: MessageEditedTimestampProps,
) => {
- const { message: propMessage, timestamp } = props;
+ const { message: propMessage, MessageTimestamp } = props;
const {
theme: {
colors: { grey },
@@ -41,7 +41,12 @@ export const MessageEditedTimestamp = <
return (
{t('Edited') + ' '}
-
+ {MessageTimestamp && (
+
+ )}
);
};
diff --git a/package/src/components/Message/MessageSimple/MessageFooter.tsx b/package/src/components/Message/MessageSimple/MessageFooter.tsx
index 3f048e5a7b..b16e2dcf6f 100644
--- a/package/src/components/Message/MessageSimple/MessageFooter.tsx
+++ b/package/src/components/Message/MessageSimple/MessageFooter.tsx
@@ -5,8 +5,6 @@ import type { Attachment } from 'stream-chat';
import type { MessageStatusProps } from './MessageStatus';
-import { MessageTimestamp } from './MessageTimestamp';
-
import type { ChannelContextValue } from '../../../contexts/channelContext/ChannelContext';
import {
Alignment,
@@ -46,7 +44,10 @@ type MessageFooterPropsWithContext<
> &
Pick<
MessagesContextValue,
- 'deletedMessagesVisibilityType' | 'MessageEditedTimestamp' | 'MessageStatus'
+ | 'deletedMessagesVisibilityType'
+ | 'MessageEditedTimestamp'
+ | 'MessageStatus'
+ | 'MessageTimestamp'
> &
MessageFooterComponentProps;
@@ -98,6 +99,7 @@ const MessageFooterWithContext = <
message,
MessageEditedTimestamp,
MessageStatus,
+ MessageTimestamp,
otherAttachments,
showMessageStatus,
} = props;
@@ -118,7 +120,7 @@ const MessageFooterWithContext = <
{deletedMessagesVisibilityType === 'sender' && (
)}
-
+
);
}
@@ -137,7 +139,7 @@ const MessageFooterWithContext = <
{message.user.name}
) : null}
{showMessageStatus && }
-
+
{isEditedMessage(message) && !isEditedMessageOpen && (
<>
@@ -160,7 +162,7 @@ const MessageFooterWithContext = <
)}
{isEditedMessageOpen && (
-
+
)}
>
);
@@ -270,7 +272,7 @@ export const MessageFooter = <
showMessageStatus,
} = useMessageContext();
- const { deletedMessagesVisibilityType, MessageEditedTimestamp, MessageStatus } =
+ const { deletedMessagesVisibilityType, MessageEditedTimestamp, MessageStatus, MessageTimestamp } =
useMessagesContext();
return (
@@ -284,6 +286,7 @@ export const MessageFooter = <
message,
MessageEditedTimestamp,
MessageStatus,
+ MessageTimestamp,
otherAttachments,
showMessageStatus,
}}
diff --git a/package/src/components/Message/MessageSimple/MessageTimestamp.tsx b/package/src/components/Message/MessageSimple/MessageTimestamp.tsx
index f1ccc2afd4..79958a18bd 100644
--- a/package/src/components/Message/MessageSimple/MessageTimestamp.tsx
+++ b/package/src/components/Message/MessageSimple/MessageTimestamp.tsx
@@ -1,27 +1,14 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import { StyleSheet, Text } from 'react-native';
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
import {
- TDateTimeParserInput,
TranslationContextValue,
useTranslationContext,
} from '../../../contexts/translationContext/TranslationContext';
-import { getDateString } from '../../../utils/getDateString';
+import { getDateString } from '../../../utils/i18n/getDateString';
export type MessageTimestampProps = Partial> & {
- /**
- * Whether to show the time in Calendar time format. Calendar time displays time relative to a today's date.
- */
- calendar?: boolean;
- /**
- * The format in which the date should be displayed.
- */
- format?: string;
- /**
- * A function to format the date.
- */
- formatDate?: (date: TDateTimeParserInput) => string;
/**
* Already Formatted date
*/
@@ -30,17 +17,21 @@ export type MessageTimestampProps = Partial {
const {
- calendar,
- format,
- formatDate,
formattedDate,
tDateTimeParser: propsTDateTimeParser,
timestamp,
+ timestampTranslationKey = 'timestamp/MessageTimestamp',
} = props;
+ const { t, tDateTimeParser: contextTDateTimeParser } = useTranslationContext();
+ const tDateTimeParser = propsTDateTimeParser || contextTDateTimeParser;
const {
theme: {
@@ -50,7 +41,17 @@ export const MessageTimestamp = (props: MessageTimestampProps) => {
},
},
} = useTheme();
- const { tDateTimeParser: contextTDateTimeParser } = useTranslationContext();
+
+ const dateString = useMemo(
+ () =>
+ getDateString({
+ date: timestamp,
+ t,
+ tDateTimeParser,
+ timestampTranslationKey,
+ }),
+ [timestamp, t, tDateTimeParser, timestampTranslationKey],
+ );
if (formattedDate) {
return (
@@ -58,16 +59,6 @@ export const MessageTimestamp = (props: MessageTimestampProps) => {
);
}
- if (!timestamp) return null;
-
- const dateString = getDateString({
- calendar,
- date: timestamp,
- format,
- formatDate,
- tDateTimeParser: propsTDateTimeParser || contextTDateTimeParser,
- });
-
if (!dateString) return null;
return {dateString.toString()};
diff --git a/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js b/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js
index 650d9de55e..9049de8d4f 100644
--- a/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js
+++ b/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js
@@ -5,7 +5,7 @@ import { cleanup, render, waitFor } from '@testing-library/react-native';
import { generateMessage } from '../../../../mock-builders/generator/message';
import { generateStaticUser, generateUser } from '../../../../mock-builders/generator/user';
import { getTestClientWithUser } from '../../../../mock-builders/mock';
-import { Streami18n } from '../../../../utils/Streami18n';
+import { Streami18n } from '../../../../utils/i18n/Streami18n';
import { Chat } from '../../../Chat/Chat';
import { MessageStatus } from '../MessageStatus';
diff --git a/package/src/components/MessageList/DateHeader.tsx b/package/src/components/MessageList/DateHeader.tsx
index 52c8025765..3530f97ca7 100644
--- a/package/src/components/MessageList/DateHeader.tsx
+++ b/package/src/components/MessageList/DateHeader.tsx
@@ -21,7 +21,7 @@ const styles = StyleSheet.create({
});
export type DateHeaderProps = {
- dateString: string | number;
+ dateString?: string | number;
};
export const DateHeader = ({ dateString }: DateHeaderProps) => {
diff --git a/package/src/components/MessageList/InlineDateSeparator.tsx b/package/src/components/MessageList/InlineDateSeparator.tsx
index 19444cfcca..d1f1f63e34 100644
--- a/package/src/components/MessageList/InlineDateSeparator.tsx
+++ b/package/src/components/MessageList/InlineDateSeparator.tsx
@@ -1,9 +1,9 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { useTheme } from '../../contexts/themeContext/ThemeContext';
import { useTranslationContext } from '../../contexts/translationContext/TranslationContext';
-import { getDateString } from '../../utils/getDateString';
+import { getDateString } from '../../utils/i18n/getDateString';
const styles = StyleSheet.create({
container: {
@@ -22,7 +22,13 @@ const styles = StyleSheet.create({
},
});
+/**
+ * Props for the `InlineDateSeparator` component.
+ */
export type InlineDateSeparatorProps = {
+ /**
+ * Date to be displayed.
+ */
date?: Date;
};
@@ -33,19 +39,18 @@ export const InlineDateSeparator = ({ date }: InlineDateSeparatorProps) => {
inlineDateSeparator: { container, text },
},
} = useTheme();
- const { tDateTimeParser } = useTranslationContext();
-
- if (!date) {
- return null;
- }
-
- const dateFormat = date.getFullYear() === new Date().getFullYear() ? 'MMM D' : 'MMM D, YYYY';
-
- const dateString = getDateString({
- date,
- format: dateFormat,
- tDateTimeParser,
- });
+ const { t, tDateTimeParser } = useTranslationContext();
+
+ const dateString = useMemo(
+ () =>
+ getDateString({
+ date,
+ t,
+ tDateTimeParser,
+ timestampTranslationKey: 'timestamp/InlineDateSeparator',
+ }),
+ [date, t, tDateTimeParser],
+ );
return (
&
- Pick, 'loadMoreThread' | 'thread'> &
- Pick & {
+ Pick, 'loadMoreThread' | 'thread'> & {
/**
* Besides existing (default) UX behavior of underlying FlatList of MessageList component, if you want
* to attach some additional props to underlying FlatList, you can add it to following prop.
@@ -278,7 +272,6 @@ const MessageListWithContext = <
setTargetedMessage,
StickyHeader,
targetedMessage,
- tDateTimeParser,
thread,
threadList = false,
TypingIndicator,
@@ -1025,18 +1018,6 @@ const MessageListWithContext = <
threadList,
]);
- const stickyHeaderDateFormat =
- stickyHeaderDate?.getFullYear() === new Date().getFullYear() ? 'MMM D' : 'MMM D, YYYY';
-
- const stickyHeaderDateString = useMemo(() => {
- if (!stickyHeaderDate) return null;
- return getDateString({
- date: stickyHeaderDate,
- format: stickyHeaderDateFormat,
- tDateTimeParser,
- });
- }, [stickyHeaderDate, stickyHeaderDateFormat]);
-
const dismissImagePicker = () => {
if (!hasMoved && selectedPicker) {
setSelectedPicker(undefined);
@@ -1092,13 +1073,6 @@ const MessageListWithContext = <
[shouldApplyAndroidWorkaround, HeaderComponent],
);
- const StickyHeaderComponent = () => {
- if (!stickyHeaderDateString) return null;
- if (StickyHeader) return ;
- if (messageListLengthAfterUpdate) return ;
- return null;
- };
-
// We need to omit the style related props from the additionalFlatListProps and add them directly instead of spreading
let additionalFlatListPropsExcludingStyle:
| Omit, 'style' | 'contentContainerStyle'>
@@ -1183,7 +1157,9 @@ const MessageListWithContext = <
{!loading && (
<>
-
+ {messageListLengthAfterUpdate && StickyHeader && (
+
+ )}
{!disableTypingIndicator && TypingIndicator && (
@@ -1253,7 +1229,6 @@ export const MessageList = <
usePaginatedMessageListContext();
const { overlay } = useOverlayContext();
const { loadMoreThread, thread } = useThreadContext();
- const { t, tDateTimeParser } = useTranslationContext();
return (
= {
/** Current [message object](https://getstream.io/chat/docs/#message_format) */
message: MessageType;
+ /**
+ * Additional styles for the system message container.
+ */
style?: StyleProp;
+ /*
+ * Lookup key in the language corresponding translations sheet to perform date formatting
+ */
};
/**
@@ -37,14 +43,20 @@ export const MessageSystem = <
},
},
} = useTheme();
- const { tDateTimeParser } = useTranslationContext();
+ const { t, tDateTimeParser } = useTranslationContext();
const createdAt = message.created_at;
- const formattedDate = getDateString({
- calendar: true,
- date: createdAt,
- tDateTimeParser,
- });
+
+ const formattedDate = useMemo(
+ () =>
+ getDateString({
+ date: createdAt,
+ t,
+ tDateTimeParser,
+ timestampTranslationKey: 'timestamp/MessageSystem',
+ }),
+ [createdAt, t, tDateTimeParser],
+ );
return (
diff --git a/package/src/components/MessageList/StickyHeader.tsx b/package/src/components/MessageList/StickyHeader.tsx
new file mode 100644
index 0000000000..472c6b709c
--- /dev/null
+++ b/package/src/components/MessageList/StickyHeader.tsx
@@ -0,0 +1,42 @@
+import React, { useMemo } from 'react';
+
+import { MessagesContextValue } from '../../contexts/messagesContext/MessagesContext';
+import { useTranslationContext } from '../../contexts/translationContext/TranslationContext';
+
+import { DefaultStreamChatGenerics } from '../../types/types';
+import { getDateString } from '../../utils/i18n/getDateString';
+
+/**
+ * Props for the StickyHeader component.
+ */
+export type StickyHeaderProps<
+ StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
+> = Pick, 'DateHeader'> & {
+ /**
+ * Date to be displayed in the sticky header.
+ */
+ date?: Date;
+ /**
+ * The formatted date string to be displayed in the sticky header.
+ */
+ dateString?: string | number;
+};
+
+export const StickyHeader = ({ date, DateHeader, dateString }: StickyHeaderProps) => {
+ const { t, tDateTimeParser } = useTranslationContext();
+
+ const stickyHeaderDateString = useMemo(() => {
+ if (dateString) return dateString;
+
+ return getDateString({
+ date,
+ t,
+ tDateTimeParser,
+ timestampTranslationKey: 'timestamp/StickyHeader',
+ });
+ }, [date]);
+
+ if (!date) return null;
+
+ return ;
+};
diff --git a/package/src/components/MessageList/__tests__/MessageSystem.test.js b/package/src/components/MessageList/__tests__/MessageSystem.test.js
index 1367142d28..d20d48f2d6 100644
--- a/package/src/components/MessageList/__tests__/MessageSystem.test.js
+++ b/package/src/components/MessageList/__tests__/MessageSystem.test.js
@@ -8,14 +8,20 @@ import { TranslationProvider } from '../../../contexts/translationContext/Transl
import { generateMessage, generateStaticMessage } from '../../../mock-builders/generator/message';
import { generateStaticUser } from '../../../mock-builders/generator/user';
-import { Streami18n } from '../../../utils/Streami18n';
+import { Streami18n } from '../../../utils/i18n/Streami18n';
import { MessageSystem } from '../MessageSystem';
afterEach(cleanup);
+let i18nInstance;
+
describe('MessageSystem', () => {
+ beforeAll(() => {
+ i18nInstance = new Streami18n();
+ });
+ afterEach(cleanup);
+
it('should render the message system', async () => {
- const i18nInstance = new Streami18n();
const translators = await i18nInstance.getTranslators();
const message = generateMessage();
const { queryByTestId } = render(
@@ -32,7 +38,6 @@ describe('MessageSystem', () => {
});
it('should match the snapshot for message system', async () => {
- const i18nInstance = new Streami18n();
const translators = await i18nInstance.getTranslators();
const user = generateStaticUser(0);
const message = generateStaticMessage('Hello World', { user });
diff --git a/package/src/components/MessageList/__tests__/ScrollToBottomButton.test.js b/package/src/components/MessageList/__tests__/ScrollToBottomButton.test.js
index 0a2d9fd9da..ad84ff3755 100644
--- a/package/src/components/MessageList/__tests__/ScrollToBottomButton.test.js
+++ b/package/src/components/MessageList/__tests__/ScrollToBottomButton.test.js
@@ -4,7 +4,7 @@ import { cleanup, fireEvent, render, waitFor } from '@testing-library/react-nati
import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext';
import { TranslationProvider } from '../../../contexts/translationContext/TranslationContext';
-import { Streami18n } from '../../../utils/Streami18n';
+import { Streami18n } from '../../../utils/i18n/Streami18n';
import { ScrollToBottomButton } from '../ScrollToBottomButton';
afterEach(cleanup);
diff --git a/package/src/components/MessageList/__tests__/TypingIndicator.test.js b/package/src/components/MessageList/__tests__/TypingIndicator.test.js
index 3072c9d063..3afe6fbb94 100644
--- a/package/src/components/MessageList/__tests__/TypingIndicator.test.js
+++ b/package/src/components/MessageList/__tests__/TypingIndicator.test.js
@@ -7,7 +7,7 @@ import { TypingProvider } from '../../../contexts/typingContext/TypingContext';
import { generateStaticUser, generateUser } from '../../../mock-builders/generator/user';
import { getTestClientWithUser } from '../../../mock-builders/mock';
-import { Streami18n } from '../../../utils/Streami18n';
+import { Streami18n } from '../../../utils/i18n/Streami18n';
import { Chat } from '../../Chat/Chat';
import { TypingIndicator } from '../TypingIndicator';
diff --git a/package/src/components/Thread/__tests__/Thread.test.js b/package/src/components/Thread/__tests__/Thread.test.js
index 1fef3a5519..edb8fedefe 100644
--- a/package/src/components/Thread/__tests__/Thread.test.js
+++ b/package/src/components/Thread/__tests__/Thread.test.js
@@ -15,7 +15,7 @@ import { generateMember } from '../../../mock-builders/generator/member';
import { generateMessage, generateStaticMessage } from '../../../mock-builders/generator/message';
import { generateStaticUser } from '../../../mock-builders/generator/user';
import { getTestClientWithUser } from '../../../mock-builders/mock';
-import { Streami18n } from '../../../utils/Streami18n';
+import { Streami18n } from '../../../utils/i18n/Streami18n';
import { Channel } from '../../Channel/Channel';
import { Chat } from '../../Chat/Chat';
import { Thread } from '../Thread';
diff --git a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap
index d32eade312..e5dff8cd6b 100644
--- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap
+++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap
@@ -1197,7 +1197,7 @@ exports[`Thread should match thread snapshot 1`] = `
]
}
>
- May 5, 2020
+ 05/05/2020
diff --git a/package/src/components/index.ts b/package/src/components/index.ts
index eb3e342f27..3a43e98a15 100644
--- a/package/src/components/index.ts
+++ b/package/src/components/index.ts
@@ -108,6 +108,7 @@ export * from './Message/MessageSimple/MessageRepliesAvatars';
export * from './Message/MessageSimple/MessageSimple';
export * from './Message/MessageSimple/MessageStatus';
export * from './Message/MessageSimple/MessageTextContainer';
+export * from './Message/MessageSimple/MessageTimestamp';
export * from './Message/MessageSimple/ReactionList';
export * from './Message/MessageSimple/utils/renderText';
export * from './Message/utils/messageActions';
diff --git a/package/src/contexts/channelContext/ChannelContext.tsx b/package/src/contexts/channelContext/ChannelContext.tsx
index e317e3cc6d..6e276058aa 100644
--- a/package/src/contexts/channelContext/ChannelContext.tsx
+++ b/package/src/contexts/channelContext/ChannelContext.tsx
@@ -4,6 +4,7 @@ import type { Channel, ChannelState } from 'stream-chat';
import type { EmptyStateProps } from '../../components/Indicators/EmptyStateIndicator';
import type { LoadingProps } from '../../components/Indicators/LoadingIndicator';
+import { StickyHeaderProps } from '../../components/MessageList/StickyHeader';
import type { DefaultStreamChatGenerics, UnknownType } from '../../types/types';
import { DEFAULT_BASE_CONTEXT_VALUE } from '../utils/defaultBaseContextValue';
@@ -180,7 +181,7 @@ export type ChannelContextValue<
*
* **Default** [DateHeader](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageList/DateHeader.tsx)
*/
- StickyHeader?: React.ComponentType<{ dateString: string | number }>;
+ StickyHeader?: React.ComponentType;
/**
* Id of message, around which Channel/MessageList gets loaded when opened.
* You will see a highlighted background for targetted message, when opened.
diff --git a/package/src/contexts/messagesContext/MessagesContext.tsx b/package/src/contexts/messagesContext/MessagesContext.tsx
index 3f5dae13b9..ed62a3c8e4 100644
--- a/package/src/contexts/messagesContext/MessagesContext.tsx
+++ b/package/src/contexts/messagesContext/MessagesContext.tsx
@@ -33,6 +33,7 @@ import type { MessageRepliesAvatarsProps } from '../../components/Message/Messag
import type { MessageSimpleProps } from '../../components/Message/MessageSimple/MessageSimple';
import type { MessageStatusProps } from '../../components/Message/MessageSimple/MessageStatus';
import type { MessageTextProps } from '../../components/Message/MessageSimple/MessageTextContainer';
+import { MessageTimestampProps } from '../../components/Message/MessageSimple/MessageTimestamp';
import type { ReactionListProps } from '../../components/Message/MessageSimple/ReactionList';
import type { MarkdownRules } from '../../components/Message/MessageSimple/utils/renderText';
import type { MessageActionsParams } from '../../components/Message/utils/messageActions';
@@ -217,6 +218,11 @@ export type MessagesContextValue<
* Defaults to: [MessageSystem](https://getstream.io/chat/docs/sdk/reactnative/ui-components/message-system/)
*/
MessageSystem: React.ComponentType>;
+ /**
+ * UI component for MessageTimestamp
+ * Defaults to: [MessageTimestamp](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/components/Message/MessageSimple/MessageTimestamp.tsx)
+ */
+ MessageTimestamp: React.ComponentType;
/**
* UI component for OverlayReactionList
*/
diff --git a/package/src/contexts/overlayContext/OverlayContext.tsx b/package/src/contexts/overlayContext/OverlayContext.tsx
index 7dfbd2d77b..56004f8281 100644
--- a/package/src/contexts/overlayContext/OverlayContext.tsx
+++ b/package/src/contexts/overlayContext/OverlayContext.tsx
@@ -8,7 +8,7 @@ import type { ImageGalleryCustomComponents } from '../../components/ImageGallery
import type { MessageType } from '../../components/MessageList/hooks/useMessageList';
import type { DefaultStreamChatGenerics } from '../../types/types';
-import type { Streami18n } from '../../utils/Streami18n';
+import type { Streami18n } from '../../utils/i18n/Streami18n';
import type { AttachmentPickerContextValue } from '../attachmentPickerContext/AttachmentPickerContext';
import type { MessageOverlayContextValue } from '../messageOverlayContext/MessageOverlayContext';
import type { DeepPartial } from '../themeContext/ThemeContext';
diff --git a/package/src/hooks/useStreami18n.ts b/package/src/hooks/useStreami18n.ts
index d123f930aa..46c0b1f809 100644
--- a/package/src/hooks/useStreami18n.ts
+++ b/package/src/hooks/useStreami18n.ts
@@ -5,7 +5,7 @@ import Dayjs from 'dayjs';
import { useIsMountedRef } from './useIsMountedRef';
import type { TranslatorFunctions } from '../contexts/translationContext/TranslationContext';
-import { Streami18n } from '../utils/Streami18n';
+import { Streami18n } from '../utils/i18n/Streami18n';
export const useStreami18n = (i18nInstance?: Streami18n) => {
const [translators, setTranslators] = useState({
diff --git a/package/src/i18n/en.json b/package/src/i18n/en.json
index 9f19f81a99..22e318a08c 100644
--- a/package/src/i18n/en.json
+++ b/package/src/i18n/en.json
@@ -74,6 +74,13 @@
"Video": "Video",
"You": "You",
"You can't send messages in this channel": "You can't send messages in this channel",
+ "timestamp/ChannelPreviewStatus": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Yesterday]\", \"lastWeek\":\"dddd\", \"nextDay\":\"[Tomorrow]\", \"nextWeek\":\"dddd [at] LT\", \"sameDay\":\"LT\", \"sameElse\":\"L\"}) }}",
+ "timestamp/ImageGalleryHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/InlineDateSeparator": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}",
+ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
"{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} and {{ nonSelfUserLength }} more are typing",
"{{ index }} of {{ photoLength }}": "{{ index }} of {{ photoLength }}",
"{{ replyCount }} Replies": "{{ replyCount }} Replies",
diff --git a/package/src/i18n/es.json b/package/src/i18n/es.json
index b5d7916bf8..95fd991062 100644
--- a/package/src/i18n/es.json
+++ b/package/src/i18n/es.json
@@ -74,6 +74,13 @@
"Video": "Video",
"You": "Tú",
"You can't send messages in this channel": "No puedes enviar mensajes en este canal",
+ "timestamp/ChannelPreviewStatus": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Ayer]\", \"lastWeek\":\"dddd\", \"nextDay\":\"[Mañana]\", \"nextWeek\":\"dddd [a las] LT\", \"sameDay\":\"LT\", \"sameElse\":\"L\"}) }}",
+ "timestamp/ImageGalleryHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/InlineDateSeparator": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}",
+ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
"{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} y {{ nonSelfUserLength }} más están escribiendo",
"{{ index }} of {{ photoLength }}": "{{ index }} de {{ photoLength }}",
"{{ replyCount }} Replies": "{{ replyCount }} Respuestas",
diff --git a/package/src/i18n/fr.json b/package/src/i18n/fr.json
index bdabdb5eaa..5a336e162a 100644
--- a/package/src/i18n/fr.json
+++ b/package/src/i18n/fr.json
@@ -74,6 +74,13 @@
"Video": "Vidéo",
"You": "Toi",
"You can't send messages in this channel": "You can't send messages in this channel",
+ "timestamp/ChannelPreviewStatus": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Hier]\", \"lastWeek\":\"dddd\", \"nextDay\":\"[Demain]\", \"nextWeek\":\"dddd [à] LT\", \"sameDay\":\"LT\", \"sameElse\":\"L\"}) }}",
+ "timestamp/ImageGalleryHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/InlineDateSeparator": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}",
+ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
"{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} et {{ nonSelfUserLength }} autres sont en train d'écrire",
"{{ index }} of {{ photoLength }}": "{{ index }} sur {{ photoLength }}",
"{{ replyCount }} Replies": "{{ replyCount }} Réponses",
diff --git a/package/src/i18n/he.json b/package/src/i18n/he.json
index 9dd83b7df4..d18cddad33 100644
--- a/package/src/i18n/he.json
+++ b/package/src/i18n/he.json
@@ -74,6 +74,13 @@
"Video": "וִידֵאוֹ",
"You": "את/ה",
"You can't send messages in this channel": "את/ב לא יכול/ה לשלוח הודעות בשיחה זו",
+ "timestamp/ChannelPreviewStatus": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[אתמול]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[מחר]\",\"nextWeek\":\"dddd [בשעה] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}",
+ "timestamp/ImageGalleryHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/InlineDateSeparator": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}",
+ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
"{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} ו-{{ nonSelfUserLength }} משתמש/ים אחר/ים מקלידים",
"{{ index }} of {{ photoLength }}": "{{ index }} מתוך {{ photoLength }}",
"{{ replyCount }} Replies": "{{ replyCount }} תגובות",
diff --git a/package/src/i18n/hi.json b/package/src/i18n/hi.json
index ec0cabc01e..b007f5fe59 100644
--- a/package/src/i18n/hi.json
+++ b/package/src/i18n/hi.json
@@ -74,6 +74,13 @@
"Video": "वीडियो",
"You": "आप",
"You can't send messages in this channel": "आप इस चैनल में संदेश नहीं भेज सकते",
+ "timestamp/ChannelPreviewStatus": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[कल]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[कल]\",\"nextWeek\":\"dddd [को] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}",
+ "timestamp/ImageGalleryHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/InlineDateSeparator": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}",
+ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
"{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} और {{ nonSelfUserLength }} अधिक टाइप कर रहे हैं",
"{{ index }} of {{ photoLength }}": "{{ index }} / {{ photoLength }}",
"{{ replyCount }} Replies": "{{ replyCount }} रिप्लाई",
diff --git a/package/src/i18n/it.json b/package/src/i18n/it.json
index f6c45803a1..ef20722fd2 100644
--- a/package/src/i18n/it.json
+++ b/package/src/i18n/it.json
@@ -74,6 +74,13 @@
"Video": "Video",
"You": "Tu",
"You can't send messages in this channel": "Non puoi inviare messaggi in questo canale",
+ "timestamp/ChannelPreviewStatus": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Ieri]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Domani]\",\"nextWeek\":\"dddd [alle] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}",
+ "timestamp/ImageGalleryHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/InlineDateSeparator": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}",
+ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
"{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} e altri {{ nonSelfUserLength }} stanno scrivendo",
"{{ index }} of {{ photoLength }}": "{{ index }} di {{ photoLength }}",
"{{ replyCount }} Replies": "{{ replyCount }} Risposte",
diff --git a/package/src/i18n/ja.json b/package/src/i18n/ja.json
index 6ab5bb75f3..a94708e730 100644
--- a/package/src/i18n/ja.json
+++ b/package/src/i18n/ja.json
@@ -74,6 +74,13 @@
"Video": "ビデオ",
"You": "あなた",
"You can't send messages in this channel": "このチャンネルではメッセージを送信できません",
+ "timestamp/ChannelPreviewStatus": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[昨日]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[明日]\",\"nextWeek\":\"dddd [の] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}",
+ "timestamp/ImageGalleryHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/InlineDateSeparator": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}",
+ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
"{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }}と{{ nonSelfUserLength }}人がタイピングしています",
"{{ index }} of {{ photoLength }}": "{{ index }} / {{ photoLength }}",
"{{ replyCount }} Replies": "{{ replyCount }}件の返信",
diff --git a/package/src/i18n/ko.json b/package/src/i18n/ko.json
index 3c0f53d7f2..ad9f371912 100644
--- a/package/src/i18n/ko.json
+++ b/package/src/i18n/ko.json
@@ -74,6 +74,13 @@
"Video": "동영상",
"You": "당신",
"You can't send messages in this channel": "이 채널에서는 메세지를 전송할 수 없습니다",
+ "timestamp/ChannelPreviewStatus": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[어제]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[내일]\",\"nextWeek\":\"dddd [LT에]\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}",
+ "timestamp/ImageGalleryHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/InlineDateSeparator": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}",
+ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
"{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} 외 {{ nonSelfUserLength }}명이 입력 중입니다",
"{{ index }} of {{ photoLength }}": "{{ index }} / {{ photoLength }}",
"{{ replyCount }} Replies": "{{ replyCount }} 답글",
diff --git a/package/src/i18n/nl.json b/package/src/i18n/nl.json
index a72a3240ff..acd87a08be 100644
--- a/package/src/i18n/nl.json
+++ b/package/src/i18n/nl.json
@@ -74,6 +74,13 @@
"Video": "Video",
"You": "U",
"You can't send messages in this channel": "Je kan geen berichten sturen in dit kanaal",
+ "timestamp/ChannelPreviewStatus": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Gisteren]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Morgen]\",\"nextWeek\":\"dddd [om] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}",
+ "timestamp/ImageGalleryHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/InlineDateSeparator": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}",
+ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
"{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} en {{ nonSelfUserLength }} anderen zijn aan het typen",
"{{ index }} of {{ photoLength }}": "{{ index }} van {{ photoLength }}",
"{{ replyCount }} Replies": "{{ replyCount }} Antwoorden",
diff --git a/package/src/i18n/pt-br.json b/package/src/i18n/pt-br.json
index 4c643afc46..bc544145f0 100644
--- a/package/src/i18n/pt-br.json
+++ b/package/src/i18n/pt-br.json
@@ -74,6 +74,13 @@
"Video": "Vídeo",
"You": "Você",
"You can't send messages in this channel": "Você não pode enviar mensagens neste canal",
+ "timestamp/ChannelPreviewStatus": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Ontem]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Amanhã]\",\"nextWeek\":\"dddd [às] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}",
+ "timestamp/ImageGalleryHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/InlineDateSeparator": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}",
+ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
"{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} e mais {{ nonSelfUserLength }} pessoa(s) estão digitando",
"{{ index }} of {{ photoLength }}": "{{ index }} de {{ photoLength }}",
"{{ replyCount }} Replies": "{{ replyCount }} Respostas",
diff --git a/package/src/i18n/ru.json b/package/src/i18n/ru.json
index 7fef9463f2..1d3b07a725 100644
--- a/package/src/i18n/ru.json
+++ b/package/src/i18n/ru.json
@@ -74,6 +74,13 @@
"Video": "видео",
"You": "Вы",
"You can't send messages in this channel": "Вы не можете отправлять сообщения в этот канал",
+ "timestamp/ChannelPreviewStatus": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Вчера]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Завтра]\",\"nextWeek\":\"dddd [в] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}",
+ "timestamp/ImageGalleryHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/InlineDateSeparator": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}",
+ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
"{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} и еще {{ nonSelfUserLength }} пишут",
"{{ index }} of {{ photoLength }}": "{{ index }} из {{ photoLength }}",
"{{ replyCount }} Replies": "{{ replyCount }} Ответов",
diff --git a/package/src/i18n/tr.json b/package/src/i18n/tr.json
index aad2912960..f10c13e48a 100644
--- a/package/src/i18n/tr.json
+++ b/package/src/i18n/tr.json
@@ -74,6 +74,13 @@
"Video": "Video",
"You": "Sen",
"You can't send messages in this channel": "Bu konuşmaya mesaj gönderemezsiniz",
+ "timestamp/ChannelPreviewStatus": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Dün]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Yarın]\",\"nextWeek\":\"dddd [saat] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}",
+ "timestamp/ImageGalleryHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/InlineDateSeparator": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}",
+ "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}",
+ "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}",
"{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} ve {{ nonSelfUserLength }} kişi daha yazıyor",
"{{ index }} of {{ photoLength }}": "{{ index }} / {{ photoLength }}",
"{{ replyCount }} Replies": "{{ replyCount }} Cevap",
diff --git a/package/src/index.ts b/package/src/index.ts
index 2a302bc343..24edca108c 100644
--- a/package/src/index.ts
+++ b/package/src/index.ts
@@ -1,3 +1,6 @@
+/** i18next polyfill to handle intl format for pluralization. For more info see https://www.i18next.com/misc/json-format#i-18-next-json-v4 */
+import 'intl-pluralrules';
+
export * from './components';
export * from './hooks';
export { registerNativeHandlers, NetInfo, iOS14RefreshGallerySelection } from './native';
@@ -9,7 +12,7 @@ export * from './icons';
export * from './types/types';
export * from './utils/patchMessageTextCommand';
-export * from './utils/Streami18n';
+export * from './utils/i18n/Streami18n';
export * from './utils/utils';
export * from './utils/StreamChatRN';
diff --git a/package/src/utils/__tests__/Streami18n.test.js b/package/src/utils/__tests__/Streami18n.test.js
index b923ed10aa..f84f40599d 100644
--- a/package/src/utils/__tests__/Streami18n.test.js
+++ b/package/src/utils/__tests__/Streami18n.test.js
@@ -4,7 +4,7 @@ import localeData from 'dayjs/plugin/localeData';
import frTranslations from '../../i18n/fr.json';
import nlTranslations from '../../i18n/nl.json';
-import { Streami18n } from '../Streami18n';
+import { Streami18n } from '../i18n/Streami18n';
Dayjs.extend(localeData);
@@ -183,3 +183,25 @@ describe('setLanguage - switch to french', () => {
}
});
});
+
+describe('formatters property', () => {
+ it('contains the default timestampFormatter', () => {
+ expect(new Streami18n().formatters.timestampFormatter).toBeDefined();
+ });
+ it('allows to override the default timestampFormatter', async () => {
+ const i18n = new Streami18n({
+ formatters: { timestampFormatter: () => () => 'custom' },
+ translationsForLanguage: { abc: '{{ value | timestampFormatter }}' },
+ });
+ await i18n.init();
+ expect(i18n.t('abc')).toBe('custom');
+ });
+ it('allows to add new custom formatter', async () => {
+ const i18n = new Streami18n({
+ formatters: { customFormatter: () => () => 'custom' },
+ translationsForLanguage: { abc: '{{ value | customFormatter }}' },
+ });
+ await i18n.init();
+ expect(i18n.t('abc')).toBe('custom');
+ });
+});
diff --git a/package/src/utils/getDateString.ts b/package/src/utils/getDateString.ts
deleted file mode 100644
index 04966f8d7f..0000000000
--- a/package/src/utils/getDateString.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import {
- isDayOrMoment,
- TDateTimeParser,
- TDateTimeParserInput,
-} from '../contexts/translationContext/TranslationContext';
-
-interface DateFormatterOptions {
- /**
- * Whether to show the time in Calendar time format. Calendar time displays time relative to a today's date.
- */
- calendar?: boolean;
- /**
- * The timestamp to be formatted.
- */
- date?: string | Date;
- /**
- * The format in which the date should be displayed.
- */
- format?: string;
- /**
- * A function to format the date.
- */
- formatDate?: (date: TDateTimeParserInput) => string;
- /**
- * The datetime parsing function.
- */
- tDateTimeParser?: TDateTimeParser;
-}
-
-export const noParsingFunctionWarning =
- 'MessageTimestamp was called but there is no datetime parsing function available';
-
-/**
- * Utility funcyion to format the date string.
- */
-export function getDateString({
- calendar,
- date,
- format,
- formatDate,
- tDateTimeParser,
-}: DateFormatterOptions): string | number | undefined {
- if (!date || (typeof date === 'string' && !Date.parse(date))) {
- return;
- }
-
- if (typeof formatDate === 'function') {
- return formatDate(new Date(date));
- }
-
- if (!tDateTimeParser) {
- console.log(noParsingFunctionWarning);
- return;
- }
-
- const parsedTime = tDateTimeParser(date);
-
- if (isDayOrMoment(parsedTime)) {
- /**
- * parsedTime.calendar is guaranteed on the type but is only
- * available when a user calls dayjs.extend(calendar)
- */
- return calendar && parsedTime.calendar ? parsedTime.calendar() : parsedTime.format(format);
- }
-
- return new Date(date).toDateString();
-}
diff --git a/package/src/utils/Streami18n.ts b/package/src/utils/i18n/Streami18n.ts
similarity index 90%
rename from package/src/utils/Streami18n.ts
rename to package/src/utils/i18n/Streami18n.ts
index ae8f56cc69..5b162ba70e 100644
--- a/package/src/utils/Streami18n.ts
+++ b/package/src/utils/i18n/Streami18n.ts
@@ -8,19 +8,26 @@ import i18n, { FallbackLng, TFunction } from 'i18next';
import type moment from 'moment';
-import type { TDateTimeParser } from '../contexts/translationContext/TranslationContext';
-import enTranslations from '../i18n/en.json';
-import esTranslations from '../i18n/es.json';
-import frTranslations from '../i18n/fr.json';
-import heTranslations from '../i18n/he.json';
-import hiTranslations from '../i18n/hi.json';
-import itTranslations from '../i18n/it.json';
-import jaTranslations from '../i18n/ja.json';
-import koTranslations from '../i18n/ko.json';
-import nlTranslations from '../i18n/nl.json';
-import ptBRTranslations from '../i18n/pt-br.json';
-import ruTranslations from '../i18n/ru.json';
-import trTranslations from '../i18n/tr.json';
+import { calendarFormats } from './calendarFormats';
+import {
+ CustomFormatters,
+ PredefinedFormatters,
+ predefinedFormatters,
+} from './predefinedFormatters';
+
+import type { TDateTimeParser } from '../../contexts/translationContext/TranslationContext';
+import enTranslations from '../../i18n/en.json';
+import esTranslations from '../../i18n/es.json';
+import frTranslations from '../../i18n/fr.json';
+import heTranslations from '../../i18n/he.json';
+import hiTranslations from '../../i18n/hi.json';
+import itTranslations from '../../i18n/it.json';
+import jaTranslations from '../../i18n/ja.json';
+import koTranslations from '../../i18n/ko.json';
+import nlTranslations from '../../i18n/nl.json';
+import ptBRTranslations from '../../i18n/pt-br.json';
+import ruTranslations from '../../i18n/ru.json';
+import trTranslations from '../../i18n/tr.json';
import 'dayjs/locale/es';
import 'dayjs/locale/fr';
@@ -41,7 +48,7 @@ import 'dayjs/locale/tr';
*/
import 'dayjs/locale/en';
-import type { DefaultStreamChatGenerics } from '../types/types';
+import type { DefaultStreamChatGenerics } from '../../types/types';
const defaultNS = 'translation';
const defaultLng = 'en';
@@ -49,6 +56,7 @@ const defaultLng = 'en';
Dayjs.extend(updateLocale);
Dayjs.updateLocale('en', {
+ calendar: calendarFormats.en,
format: {
L: 'DD/MM/YYYY',
LL: 'D MMMM YYYY',
@@ -59,37 +67,18 @@ Dayjs.updateLocale('en', {
},
});
-Dayjs.updateLocale('nl', {
- calendar: {
- lastDay: '[gisteren om] LT',
- lastWeek: '[afgelopen] dddd [om] LT',
- nextDay: '[morgen om] LT',
- nextWeek: 'dddd [om] LT',
- sameDay: '[vandaag om] LT',
- sameElse: 'L',
- },
+Dayjs.updateLocale('es', {
+ calendar: calendarFormats.es,
});
-
-Dayjs.updateLocale('it', {
- calendar: {
- lastDay: '[Ieri alle] LT',
- lastWeek: '[lo scorso] dddd [alle] LT',
- nextDay: '[Domani alle] LT',
- nextWeek: 'dddd [alle] LT',
- sameDay: '[Oggi alle] LT',
- sameElse: 'L',
- },
+Dayjs.updateLocale('fr', {
+ calendar: calendarFormats.fr,
+});
+Dayjs.updateLocale('he', {
+ calendar: calendarFormats.he,
});
Dayjs.updateLocale('hi', {
- calendar: {
- lastDay: '[कल] LT',
- lastWeek: '[पिछले] dddd, LT',
- nextDay: '[कल] LT',
- nextWeek: 'dddd, LT',
- sameDay: '[आज] LT',
- sameElse: 'L',
- },
+ calendar: calendarFormats.hi,
/**
* Hindi notation for meridiems are quite fuzzy in practice. While there exists
@@ -116,35 +105,26 @@ Dayjs.updateLocale('hi', {
meridiemParse: /रात|सुबह|दोपहर|शाम/,
});
-
-Dayjs.updateLocale('fr', {
- calendar: {
- lastDay: '[Hier à] LT',
- lastWeek: 'dddd [dernier à] LT',
- nextDay: '[Demain à] LT',
- nextWeek: 'dddd [à] LT',
- sameDay: '[Aujourd’hui à] LT',
- sameElse: 'L',
- },
+Dayjs.updateLocale('it', {
+ calendar: calendarFormats.it,
});
-
-Dayjs.updateLocale('tr', {
- calendar: {
- lastDay: '[dün] LT',
- lastWeek: '[geçen] dddd [saat] LT',
- nextDay: '[yarın saat] LT',
- nextWeek: '[gelecek] dddd [saat] LT',
- sameDay: '[bugün saat] LT',
- sameElse: 'L',
- },
+Dayjs.updateLocale('ja', {
+ calendar: calendarFormats.ja,
+});
+Dayjs.updateLocale('ko', {
+ calendar: calendarFormats.ko,
+});
+Dayjs.updateLocale('nl', {
+ calendar: calendarFormats.nl,
+});
+Dayjs.updateLocale('pt-br', {
+ calendar: calendarFormats['pt-br'],
});
-
Dayjs.updateLocale('ru', {
- calendar: {
- lastDay: '[Вчера, в] LT',
- nextDay: '[Завтра, в] LT',
- sameDay: '[Сегодня, в] LT',
- },
+ calendar: calendarFormats.ru,
+});
+Dayjs.updateLocale('tr', {
+ calendar: calendarFormats.tr,
});
const en_locale = {
@@ -171,11 +151,12 @@ const en_locale = {
const isDayJs = (dateTimeParser: typeof Dayjs | typeof moment): dateTimeParser is typeof Dayjs =>
(dateTimeParser as typeof Dayjs).extend !== undefined;
-type Options = {
+type Streami18nOptions = {
DateTimeParser?: typeof Dayjs | typeof moment;
dayjsLocaleConfigForLanguage?: Partial;
debug?: boolean;
disableDateTimeTranslations?: boolean;
+ formatters?: Partial & CustomFormatters;
language?: string;
logger?: (msg?: string) => void;
translationsForLanguage?: Partial;
@@ -184,7 +165,7 @@ type Options = {
type I18NextConfig = {
debug: boolean;
fallbackLng: false | FallbackLng;
- interpolation: { escapeValue: boolean };
+ interpolation: { escapeValue: boolean; formatSeparator: string };
keySeparator: false | string;
lng: string;
nsSeparator: false | string;
@@ -405,6 +386,7 @@ export class Streami18n {
logger: (msg?: string) => void;
currentLanguage: string;
DateTimeParser: typeof Dayjs | typeof moment;
+ formatters: PredefinedFormatters & CustomFormatters = predefinedFormatters;
isCustomDateTimeParser: boolean;
i18nextConfig: I18NextConfig;
@@ -434,7 +416,7 @@ export class Streami18n {
*
* @param {*} options
*/
- constructor(options: Options = {}, i18nextConfig: Partial = {}) {
+ constructor(options: Streami18nOptions = {}, i18nextConfig: Partial = {}) {
const finalOptions = {
...defaultStreami18nOptions,
...options,
@@ -445,6 +427,7 @@ export class Streami18n {
this.currentLanguage = finalOptions.language;
this.DateTimeParser = finalOptions.DateTimeParser;
+ this.formatters = { ...predefinedFormatters, ...options?.formatters };
try {
/**
@@ -491,7 +474,7 @@ export class Streami18n {
this.i18nextConfig = {
debug: finalOptions.debug,
fallbackLng: false,
- interpolation: { escapeValue: false },
+ interpolation: { escapeValue: false, formatSeparator: '|' },
keySeparator: false,
lng: this.currentLanguage,
nsSeparator: false,
@@ -556,6 +539,12 @@ export class Streami18n {
this.onTFunctionOverrideListeners.forEach((listener) => listener(this.t));
}
this.initialized = true;
+ if (this.formatters) {
+ Object.entries(this.formatters).forEach(([name, formatterFactory]) => {
+ if (!formatterFactory) return;
+ this.i18nInstance.services.formatter?.add(name, formatterFactory(this));
+ });
+ }
} catch (error) {
this.logger(`Something went wrong with init: ${JSON.stringify(error)}`);
}
diff --git a/package/src/utils/i18n/calendarFormats.ts b/package/src/utils/i18n/calendarFormats.ts
new file mode 100644
index 0000000000..74995e7d02
--- /dev/null
+++ b/package/src/utils/i18n/calendarFormats.ts
@@ -0,0 +1,110 @@
+type CalendarFormats = {
+ lastDay: string;
+ lastWeek: string;
+ nextDay: string;
+ nextWeek: string;
+ sameDay: string;
+ sameElse: string;
+};
+
+/**
+ * Calendar formats for different languages.
+ */
+export const calendarFormats: Record = {
+ en: {
+ lastDay: '[Yesterday]',
+ lastWeek: 'dddd',
+ nextDay: '[Tomorrow]',
+ nextWeek: 'dddd [at] LT',
+ sameDay: '[Today]',
+ sameElse: 'L',
+ },
+ es: {
+ lastDay: '[Ayer]',
+ lastWeek: 'dddd',
+ nextDay: '[Mañana]',
+ nextWeek: 'dddd [a las] LT',
+ sameDay: '[Hoy]',
+ sameElse: 'L',
+ },
+ fr: {
+ lastDay: '[Hier]',
+ lastWeek: 'dddd',
+ nextDay: '[Demain]',
+ nextWeek: 'dddd [à] LT',
+ sameDay: "[Aujourd'hui]",
+ sameElse: 'L',
+ },
+ he: {
+ lastDay: '[אתמול]',
+ lastWeek: 'dddd',
+ nextDay: '[מחר]',
+ nextWeek: 'dddd [בשעה] LT',
+ sameDay: '[היום]',
+ sameElse: 'L',
+ },
+ hi: {
+ lastDay: '[कल]',
+ lastWeek: 'dddd',
+ nextDay: '[कल]',
+ nextWeek: 'dddd [को] LT',
+ sameDay: '[आज]',
+ sameElse: 'L',
+ },
+ it: {
+ lastDay: '[Ieri]',
+ lastWeek: 'dddd',
+ nextDay: '[Domani]',
+ nextWeek: 'dddd [alle] LT',
+ sameDay: '[Oggi]',
+ sameElse: 'L',
+ },
+ ja: {
+ lastDay: '[昨日]',
+ lastWeek: 'dddd',
+ nextDay: '[明日]',
+ nextWeek: 'dddd [の] LT',
+ sameDay: '[今日]',
+ sameElse: 'L',
+ },
+ ko: {
+ lastDay: '[어제]',
+ lastWeek: 'dddd',
+ nextDay: '[내일]',
+ nextWeek: 'dddd [LT에]',
+ sameDay: '[오늘]',
+ sameElse: 'L',
+ },
+ nl: {
+ lastDay: '[Gisteren]',
+ lastWeek: 'dddd',
+ nextDay: '[Morgen]',
+ nextWeek: 'dddd [om] LT',
+ sameDay: '[Vandaag]',
+ sameElse: 'L',
+ },
+ 'pt-br': {
+ lastDay: '[Ontem]',
+ lastWeek: 'dddd',
+ nextDay: '[Amanhã]',
+ nextWeek: 'dddd [às] LT',
+ sameDay: '[Hoje]',
+ sameElse: 'L',
+ },
+ ru: {
+ lastDay: '[Вчера]',
+ lastWeek: 'dddd',
+ nextDay: '[Завтра]',
+ nextWeek: 'dddd [в] LT',
+ sameDay: '[Сегодня]',
+ sameElse: 'L', // L is the localized date format
+ },
+ tr: {
+ lastDay: '[Dün]',
+ lastWeek: 'dddd',
+ nextDay: '[Yarın]',
+ nextWeek: 'dddd [saat] LT',
+ sameDay: '[Bugün]',
+ sameElse: 'L',
+ },
+};
diff --git a/package/src/utils/i18n/getDateString.ts b/package/src/utils/i18n/getDateString.ts
new file mode 100644
index 0000000000..6b16e8f943
--- /dev/null
+++ b/package/src/utils/i18n/getDateString.ts
@@ -0,0 +1,76 @@
+import type { TimestampFormatterOptions } from './predefinedFormatters';
+
+import {
+ isDayOrMoment,
+ TranslatorFunctions,
+} from '../../contexts/translationContext/TranslationContext';
+
+type DateFormatterOptions = TimestampFormatterOptions &
+ Partial & {
+ /**
+ * The timestamp to be formatted.
+ */
+ date?: string | Date;
+ /*
+ * Lookup key in the language corresponding translations sheet to perform date formatting
+ */
+ timestampTranslationKey?: string;
+ };
+
+export const noParsingFunctionWarning =
+ 'MessageTimestamp was called but there is no datetime parsing function available';
+
+/**
+ * Utility function to format the date string.
+ */
+export function getDateString({
+ calendar,
+ calendarFormats,
+ date,
+ format,
+ t,
+ tDateTimeParser,
+ timestampTranslationKey,
+}: DateFormatterOptions): string | number | undefined {
+ if (!date || (typeof date === 'string' && !Date.parse(date))) {
+ return;
+ }
+
+ if (!tDateTimeParser) {
+ console.log(noParsingFunctionWarning);
+ return;
+ }
+
+ if (t && timestampTranslationKey) {
+ const options: TimestampFormatterOptions = {};
+ if (typeof calendar !== 'undefined' && calendar !== null) {
+ options.calendar = calendar;
+ }
+ if (typeof calendarFormats !== 'undefined' && calendarFormats !== null) {
+ options.calendarFormats = calendarFormats;
+ }
+ if (typeof format !== 'undefined' && format !== null) {
+ options.format = format;
+ }
+ const translatedTimestamp = t(timestampTranslationKey, {
+ ...options,
+ timestamp: new Date(date),
+ });
+ const translationKeyFound = timestampTranslationKey !== translatedTimestamp;
+ if (translationKeyFound) return translatedTimestamp;
+ }
+
+ const parsedTime = tDateTimeParser(date);
+
+ if (isDayOrMoment(parsedTime)) {
+ /**
+ * parsedTime.calendar is guaranteed on the type but is only
+ * available when a user calls dayjs.extend(calendar)
+ */
+ return calendar && parsedTime.calendar
+ ? parsedTime.calendar(undefined, calendarFormats)
+ : parsedTime.format(format);
+ }
+
+ return new Date(date).toDateString();
+}
diff --git a/package/src/utils/i18n/predefinedFormatters.ts b/package/src/utils/i18n/predefinedFormatters.ts
new file mode 100644
index 0000000000..3b1c3998ec
--- /dev/null
+++ b/package/src/utils/i18n/predefinedFormatters.ts
@@ -0,0 +1,63 @@
+import { getDateString } from './getDateString';
+import { Streami18n } from './Streami18n';
+
+export type TimestampFormatterOptions = {
+ /* If true, call the `Day.js` calendar function to get the date string to display (e.g. "Yesterday at 3:58 PM"). */
+ calendar?: boolean | null;
+ /* Object specifying date display formats for dates formatted with calendar extension. Active only if calendar prop enabled. */
+ calendarFormats?: Record;
+ /* Overrides the default timestamp format if calendar is disabled. */
+ format?: string;
+};
+
+export type FormatterFactory = (
+ streamI18n: Streami18n,
+) => (value: V, lng: string | undefined, options: Record) => string;
+
+// Here is any used, because we do not want to enforce any specific rules and
+// want to leave the type declaration to the integrator
+/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+export type CustomFormatters = Record>;
+
+export type PredefinedFormatters = {
+ timestampFormatter: FormatterFactory;
+};
+
+export const predefinedFormatters: PredefinedFormatters = {
+ timestampFormatter:
+ (streamI18n) =>
+ (
+ value,
+ _,
+ {
+ calendarFormats,
+ ...options
+ }: Pick & {
+ calendarFormats?: Record | string;
+ },
+ ) => {
+ let parsedCalendarFormats;
+ try {
+ if (!options.calendar) {
+ parsedCalendarFormats = {};
+ } else if (typeof calendarFormats === 'string') {
+ parsedCalendarFormats = JSON.parse(calendarFormats);
+ } else if (typeof calendarFormats === 'object') {
+ parsedCalendarFormats = calendarFormats;
+ }
+ } catch (e) {
+ console.error('[TIMESTAMP FORMATTER]', e);
+ }
+
+ const result = getDateString({
+ ...options,
+ calendarFormats: parsedCalendarFormats,
+ date: value,
+ tDateTimeParser: streamI18n.tDateTimeParser,
+ });
+ if (!result || typeof result === 'number') {
+ return JSON.stringify(value);
+ }
+ return result;
+ },
+};
diff --git a/package/yarn.lock b/package/yarn.lock
index d729819cbb..6ccecd67fa 100644
--- a/package/yarn.lock
+++ b/package/yarn.lock
@@ -1767,13 +1767,6 @@
dependencies:
regenerator-runtime "^0.14.0"
-"@babel/runtime@^7.12.0", "@babel/runtime@^7.8.4":
- version "7.22.5"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.5.tgz#8564dd588182ce0047d55d7a75e93921107b57ec"
- integrity sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==
- dependencies:
- regenerator-runtime "^0.13.11"
-
"@babel/runtime@^7.16.3":
version "7.23.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7"
@@ -1781,6 +1774,13 @@
dependencies:
regenerator-runtime "^0.14.0"
+"@babel/runtime@^7.17.2", "@babel/runtime@^7.23.2":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12"
+ integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==
+ dependencies:
+ regenerator-runtime "^0.14.0"
+
"@babel/runtime@^7.20.0":
version "7.24.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd"
@@ -1788,12 +1788,12 @@
dependencies:
regenerator-runtime "^0.14.0"
-"@babel/runtime@^7.23.2":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12"
- integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==
+"@babel/runtime@^7.8.4":
+ version "7.22.5"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.5.tgz#8564dd588182ce0047d55d7a75e93921107b57ec"
+ integrity sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==
dependencies:
- regenerator-runtime "^0.14.0"
+ regenerator-runtime "^0.13.11"
"@babel/template@^7.0.0", "@babel/template@^7.22.5", "@babel/template@^7.3.3":
version "7.22.5"
@@ -6867,12 +6867,12 @@ i18next-parser@^9.0.0:
vinyl "~3.0.0"
vinyl-fs "^4.0.0"
-i18next@20.2.4:
- version "20.2.4"
- resolved "https://registry.yarnpkg.com/i18next/-/i18next-20.2.4.tgz#972220f19dfef0075a70890d3e8b1f7cf64c5bd6"
- integrity sha512-goE1LCA/IZOGG26PkkqoOl2KWR7YP606SvokVQZ29J6QwE02KycrzNetoMUJeqYrTxs4rmiiZgZp+q8qofQL6Q==
+i18next@^21.6.14:
+ version "21.10.0"
+ resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.10.0.tgz#85429af55fdca4858345d0e16b584ec29520197d"
+ integrity sha512-YeuIBmFsGjUfO3qBmMOc0rQaun4mIpGKET5WDwvu8lU7gvwpcariZLNtL0Fzj+zazcHUrlXHiptcFhBMFaxzfg==
dependencies:
- "@babel/runtime" "^7.12.0"
+ "@babel/runtime" "^7.17.2"
i18next@^23.5.1:
version "23.11.5"
@@ -7005,6 +7005,11 @@ internal-slot@^1.0.7:
hasown "^2.0.0"
side-channel "^1.0.4"
+intl-pluralrules@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/intl-pluralrules/-/intl-pluralrules-2.0.1.tgz#de16c3df1e09437635829725e88ea70c9ad79569"
+ integrity sha512-astxTLzIdXPeN0K9Rumi6LfMpm3rvNO0iJE+h/k8Kr/is+wPbRe4ikyDjlLr6VTh/mEfNv8RjN+gu3KwDiuhqg==
+
invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"