Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: address the circular dependencies among TranslationContext and Streami18n #2483

Merged
merged 3 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@
}
},
"sideEffects": [
"*.css"
"*.css",
"./dist/i18n/Streami18n.js"
],
"keywords": [
"chat",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useMemo } from 'react';

import { isDate, isDayOrMoment } from '../../../context/TranslationContext';
import { isDate, isDayOrMoment } from '../../../i18n';

import type { ChannelStateContextValue } from '../../../context/ChannelStateContext';

Expand Down
4 changes: 2 additions & 2 deletions src/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { useCreateChatContext } from './hooks/useCreateChatContext';
import { useChannelsQueryState } from './hooks/useChannelsQueryState';

import { ChatProvider, CustomClasses } from '../../context/ChatContext';
import { SupportedTranslations, TranslationProvider } from '../../context/TranslationContext';
import { TranslationProvider } from '../../context/TranslationContext';

import type { StreamChat } from 'stream-chat';

import type { SupportedTranslations } from '../../i18n/types';
import type { Streami18n } from '../../i18n/Streami18n';

import type { DefaultStreamChatGenerics } from '../../types/types';

export type ChatProps<
Expand Down
6 changes: 3 additions & 3 deletions src/components/Chat/hooks/useChat.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useCallback, useEffect, useRef, useState } from 'react';

import { TranslationContextValue } from '../../../context/TranslationContext';
import {
defaultDateTimeParser,
isLanguageSupported,
Streami18n,
SupportedTranslations,
TranslationContextValue,
} from '../../../context/TranslationContext';
import { Streami18n } from '../../../i18n';
} from '../../../i18n';
import { version } from '../../../version';

import type { AppSettingsAPIResponse, Channel, Event, Mute, StreamChat } from 'stream-chat';
Expand Down
4 changes: 3 additions & 1 deletion src/components/DateSeparator/DateSeparator.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React from 'react';

import { useTranslationContext } from '../../context/TranslationContext';
import { getDateString, TimestampFormatterOptions } from '../../i18n/utils';
import { getDateString } from '../../i18n/utils';

import type { TimestampFormatterOptions } from '../../i18n/types';

export type DateSeparatorProps = TimestampFormatterOptions & {
/** The date to format */
Expand Down
4 changes: 2 additions & 2 deletions src/components/EventComponent/EventComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import React from 'react';
import { AvatarProps, Avatar as DefaultAvatar } from '../Avatar';

import { useTranslationContext } from '../../context/TranslationContext';
import { getDateString } from '../../i18n/utils';

import type { StreamMessage } from '../../context/ChannelStateContext';

import type { DefaultStreamChatGenerics } from '../../types/types';
import { getDateString, TimestampFormatterOptions } from '../../i18n/utils';
import type { TimestampFormatterOptions } from '../../i18n/types';

export type EventComponentProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
Expand Down
2 changes: 1 addition & 1 deletion src/components/Message/MessageTimestamp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { Timestamp as DefaultTimestamp } from './Timestamp';
import { useComponentContext } from '../../context';

import type { StreamMessage } from '../../context/ChannelStateContext';
import type { TimestampFormatterOptions } from '../../i18n/types';
import type { DefaultStreamChatGenerics } from '../../types/types';
import type { TimestampFormatterOptions } from '../../i18n/utils';

export type MessageTimestampProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
Expand Down
5 changes: 3 additions & 2 deletions src/components/Message/Timestamp.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { useMemo } from 'react';

import { useMessageContext } from '../../context/MessageContext';
import { isDate, useTranslationContext } from '../../context/TranslationContext';
import { getDateString, TimestampFormatterOptions } from '../../i18n/utils';
import { useTranslationContext } from '../../context/TranslationContext';
import { getDateString, isDate } from '../../i18n/utils';
import type { TimestampFormatterOptions } from '../../i18n/types';

export interface TimestampProps extends TimestampFormatterOptions {
/* Adds a CSS class name to the component's outer `time` container. */
Expand Down
8 changes: 5 additions & 3 deletions src/components/Message/__tests__/MessageTimestamp.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ const calendarFormats = {
const dateMock = 'the date';
const formatDate = () => dateMock;

const createdAt = new Date('2019-04-03T14:42:47.087869Z');

const messageMock = generateMessage({
created_at: new Date('2019-04-03T14:42:47.087869Z'),
created_at: createdAt,
});

const renderComponent = async ({ chatProps, componentCtx, messageCtx, props } = {}) => {
Expand Down Expand Up @@ -124,7 +126,7 @@ describe('<MessageTimestamp />', () => {
}),
},
});
expect(container).toHaveTextContent('2019-04-03T14:42:47+00:00');
expect(container).toHaveTextContent('2019-04-03T14:42:47Z');
});

it('should render with custom format provided via i18n service', async () => {
Expand Down Expand Up @@ -172,7 +174,7 @@ describe('<MessageTimestamp />', () => {
},
props: { calendarFormats },
});
expect(container).toHaveTextContent('2019-04-03T14:42:47+00:00');
expect(container).toHaveTextContent('2019-04-03T14:42:47Z');
});

it('should reflect the custom calendarFormats if calendar is enabled', async () => {
Expand Down
5 changes: 2 additions & 3 deletions src/components/MessageList/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
import { nanoid } from 'nanoid';

import { CUSTOM_MESSAGE_TYPE } from '../../constants/messageTypes';

import { isDate } from '../../context/TranslationContext';
import { isMessageEdited } from '../Message/utils';
import { isDate } from '../../i18n';

import type { MessageLabel, UserResponse } from 'stream-chat';
import type { DefaultStreamChatGenerics } from '../../types/types';
import type { StreamMessage } from '../../context/ChannelStateContext';
import { isMessageEdited } from '../Message/utils';

type ProcessMessagesContext = {
/** the connected user ID */
Expand Down
40 changes: 2 additions & 38 deletions src/context/TranslationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,23 @@ import calendar from 'dayjs/plugin/calendar';
import localizedFormat from 'dayjs/plugin/localizedFormat';

import { getDisplayName } from './utils/getDisplayName';
import { defaultDateTimeParser, defaultTranslatorFunction } from '../i18n/utils';

import type { TFunction } from 'i18next';
import type { Moment } from 'moment-timezone';
import type { TranslationLanguages } from 'stream-chat';

import type { UnknownType } from '../types/types';
import { defaultTranslatorFunction } from '../i18n';
import type { TDateTimeParser } from '../i18n/types';

Dayjs.extend(calendar);
Dayjs.extend(localizedFormat);

export type SupportedTranslations =
| 'de'
| 'en'
| 'es'
| 'fr'
| 'hi'
| 'it'
| 'ja'
| 'ko'
| 'nl'
| 'pt'
| 'ru'
| 'tr';

export const isLanguageSupported = (language: string): language is SupportedTranslations => {
const translations = ['de', 'en', 'es', 'fr', 'hi', 'it', 'ja', 'ko', 'nl', 'pt', 'ru', 'tr'];
return translations.some((translation) => language === translation);
};

export const isDayOrMoment = (output: TDateTimeParserOutput): output is Dayjs.Dayjs | Moment =>
!!(output as Dayjs.Dayjs | Moment)?.isSame;

export const isDate = (output: TDateTimeParserOutput): output is Date =>
!!(output as Date)?.getMonth;

export const isNumberOrString = (output: TDateTimeParserOutput): output is number | string =>
typeof output === 'string' || typeof output === 'number';

export type TDateTimeParserInput = string | number | Date;

export type TDateTimeParserOutput = string | number | Date | Dayjs.Dayjs | Moment;

export type TDateTimeParser = (input?: TDateTimeParserInput) => TDateTimeParserOutput;

export type TranslationContextValue = {
t: TFunction;
tDateTimeParser: TDateTimeParser;
userLanguage: TranslationLanguages;
};

export const defaultDateTimeParser = (input?: TDateTimeParserInput) => Dayjs(input);

export const TranslationContext = React.createContext<TranslationContextValue>({
t: defaultTranslatorFunction,
tDateTimeParser: defaultDateTimeParser,
Expand Down
8 changes: 2 additions & 6 deletions src/i18n/Streami18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@ import localeData from 'dayjs/plugin/localeData';
import relativeTime from 'dayjs/plugin/relativeTime';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import { predefinedFormatters } from './utils';
import { defaultTranslatorFunction, predefinedFormatters } from './utils';

import type momentTimezone from 'moment-timezone';
import type { TranslationLanguages } from 'stream-chat';

import type { CustomFormatters, PredefinedFormatters } from './utils';
import type { TDateTimeParser } from '../context/TranslationContext';

import type { UnknownType } from '../types/types';
import type { CustomFormatters, PredefinedFormatters, TDateTimeParser } from './types';

import {
deTranslations,
Expand Down Expand Up @@ -418,8 +416,6 @@ const defaultStreami18nOptions = {
logger: (message?: string) => console.warn(message),
};

export const defaultTranslatorFunction: TFunction = <tResult = string>(key: tResult) => key;

export class Streami18n {
i18nInstance = i18n.createInstance();
Dayjs = null;
Expand Down
10 changes: 9 additions & 1 deletion src/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
export * from './translations';
export * from './Streami18n';
export type { FormatterFactory, TimestampFormatterOptions } from './utils';
export {
defaultDateTimeParser,
defaultTranslatorFunction,
isDate,
isDayOrMoment,
isLanguageSupported,
isNumberOrString,
} from './utils';
export * from './types';
53 changes: 53 additions & 0 deletions src/i18n/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Streami18n } from './Streami18n';
import Dayjs from 'dayjs';
import type { Moment } from 'moment-timezone';
import { MessageContextValue } from '../context';
import type { TFunction } from 'i18next';

export type FormatterFactory<V> = (
streamI18n: Streami18n,
) => (value: V, lng: string | undefined, options: Record<string, unknown>) => string;

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;
/* Object specifying date display formats for dates formatted with calendar extension. Active only if calendar prop enabled. */
calendarFormats?: Record<string, string>;
/* Overrides the default timestamp format if calendar is disabled. */
format?: string;
};

export type TDateTimeParserInput = string | number | Date;
export type TDateTimeParserOutput = string | number | Date | Dayjs.Dayjs | Moment;
export type TDateTimeParser = (input?: TDateTimeParserInput) => TDateTimeParserOutput;

export type SupportedTranslations =
| 'de'
| 'en'
| 'es'
| 'fr'
| 'hi'
| 'it'
| 'ja'
| 'ko'
| 'nl'
| 'pt'
| 'ru'
| 'tr';

export type DateFormatterOptions = TimestampFormatterOptions & {
formatDate?: MessageContextValue['formatDate'];
messageCreatedAt?: string | Date;
t?: TFunction;
tDateTimeParser?: TDateTimeParser;
timestampTranslationKey?: 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<string, FormatterFactory<any>>;

export type PredefinedFormatters = {
timestampFormatter: FormatterFactory<string | Date>;
};
66 changes: 28 additions & 38 deletions src/i18n/utils.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,30 @@
import {
isDate,
isDayOrMoment,
isNumberOrString,
MessageContextValue,
TDateTimeParser,
} from '../context';
import Dayjs from 'dayjs';

import type { TFunction } from 'i18next';
import type { 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;
/* Object specifying date display formats for dates formatted with calendar extension. Active only if calendar prop enabled. */
calendarFormats?: Record<string, string>;
/* Overrides the default timestamp format if calendar is disabled. */
format?: string;
};

type DateFormatterOptions = TimestampFormatterOptions & {
formatDate?: MessageContextValue['formatDate'];
messageCreatedAt?: string | Date;
t?: TFunction;
tDateTimeParser?: TDateTimeParser;
timestampTranslationKey?: string;
};
import type { Moment } from 'moment-timezone';
import type {
DateFormatterOptions,
PredefinedFormatters,
SupportedTranslations,
TDateTimeParserInput,
TDateTimeParserOutput,
TimestampFormatterOptions,
} from './types';

export const notValidDateWarning =
'MessageTimestamp was called without a message, or message has invalid created_at date.';
export const noParsingFunctionWarning =
'MessageTimestamp was called but there is no datetime parsing function available';

export const isNumberOrString = (output: TDateTimeParserOutput): output is number | string =>
typeof output === 'string' || typeof output === 'number';

export const isDayOrMoment = (output: TDateTimeParserOutput): output is Dayjs.Dayjs | Moment =>
!!(output as Dayjs.Dayjs | Moment)?.isSame;

export const isDate = (output: TDateTimeParserOutput): output is Date =>
!!(output as Date)?.getMonth;

export function getDateString({
calendar,
calendarFormats,
Expand Down Expand Up @@ -96,19 +90,6 @@ export function getDateString({
return null;
}

export type FormatterFactory<V> = (
streamI18n: Streami18n,
) => (value: V, lng: string | undefined, options: Record<string, unknown>) => 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<string, FormatterFactory<any>>;

export type PredefinedFormatters = {
timestampFormatter: FormatterFactory<string | Date>;
};

export const predefinedFormatters: PredefinedFormatters = {
timestampFormatter: (streamI18n) => (
value,
Expand Down Expand Up @@ -145,3 +126,12 @@ export const predefinedFormatters: PredefinedFormatters = {
return result;
},
};

export const defaultTranslatorFunction: TFunction = <tResult = string>(key: tResult) => key;

export const defaultDateTimeParser = (input?: TDateTimeParserInput) => Dayjs(input);

export const isLanguageSupported = (language: string): language is SupportedTranslations => {
const translations = ['de', 'en', 'es', 'fr', 'hi', 'it', 'ja', 'ko', 'nl', 'pt', 'ru', 'tr'];
return translations.some((translation) => language === translation);
};
Loading