Skip to content

Commit

Permalink
feat: add timezone option to Streami18n instance using moment-timezone (
Browse files Browse the repository at this point in the history
#2595)

* feat: add timezone option to Streami18n instance

* fix: timezone using moment-timezone

* fix: remove moment timezone from TSMessagingApp

* fix: tests

* docs: add timezone Streami18n docs

* fix: remove dayjs timezone plugin
  • Loading branch information
khushal87 authored Jul 18, 2024
1 parent ef677b8 commit a7387a2
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 48 deletions.
30 changes: 26 additions & 4 deletions docusaurus/docs/reactnative/basics/internationalization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ streami18n.registerTranslation('nl', {
[`react-native-localize`](https://github.com/zoontek/react-native-localize#-react-native-localize) package provides a toolbox for React Native app localization. You can use this package to access user preferred locale, and use it to set language for chat components:

```tsx
import *as RNLocalizefrom 'react-native-localize';
const streami18n =new Streami18n();
import * as RNLocalize from 'react-native-localize';
const streami18n = new Streami18n();

const userPreferredLocales = RNLocalize.getLocales();

Expand Down Expand Up @@ -176,14 +176,14 @@ const i18n =new Streami18n({
Or by providing your own [Day.js](https://day.js.org/docs/en/installation/installation) object:

```tsx
import Dayjsfrom 'dayjs';
import Dayjs from 'dayjs';

import 'dayjs/locale/nl';
import 'dayjs/locale/it';
// or if you want to include all locales
import 'dayjs/min/locales';

const i18n =new Streami18n({
const i18n = new Streami18n({
language: 'nl',
DateTimeParser: Dayjs,
});
Expand All @@ -195,6 +195,28 @@ If you would like to stick with English language for date-times in Stream compon

If your application has a user-base that speaks more than one language, Stream's Chat Client provides the option to automatically translate messages. For more information on using automatic machine translation for messages, see the [Chat Client Guide on Translation](https://getstream.io/chat/docs/react-native/translation/?language=javascript).

### Timezone location

To display date and time in different than machine's local timezone, you can provide the timezone parameter to the `Streami18n` constructor. The timezone value has to be a valid [timezone identifier string](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). If no timezone parameter is provided, then the machine's local timezone is applied.

:::note
On our React Native SDK, the timezone is only supported through `moment-timezone` and not through the default `Dayjs`. This is because of the [following issue](https://github.com/iamkun/dayjs/issues/1377).

So, to ensure this please pass the `moment-timezone` object to the `DateTimeParser` key of the `Streami18n` constructor.
:::

```tsx
import { Streami18n } from 'stream-chat-react';
import momentTimezone from 'moment-timezone';

const streami18n = new Streami18n({
DateTimeParser: momentTimezone,
timezone: 'Europe/Budapest',
});
```

Moment Timezone will automatically load and extend the moment module, then return the modified instance. This will also prevent multiple versions of moment being installed in a project.

## Options

`options` are the first optional parameter passed to `Streami18n`, it is an object with all keys being optional.
Expand Down
2 changes: 1 addition & 1 deletion examples/TypeScriptMessaging/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
"@react-navigation/stack": "^6.2.0",
"@stream-io/flat-list-mvcp": "0.10.3",
"react": "18.2.0",
"react-native-audio-recorder-player": "3.6.6",
"react-native": "^0.73.6",
"react-native-audio-recorder-player": "3.6.6",
"react-native-document-picker": "^9.0.1",
"react-native-fs": "^2.18.0",
"react-native-gesture-handler": "^2.14.0",
Expand Down
8 changes: 4 additions & 4 deletions examples/TypeScriptMessaging/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6899,10 +6899,10 @@ statuses@~1.5.0:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==

[email protected].0:
version "5.33.0"
resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.33.0.tgz#14f04de90cbc8db011bab8db3fa84abe2dc2eaec"
integrity sha512-V9OJA9MrHzaCw5q16ZRbEktA1HamITbXPOkVZOjpDbb0OBcmedmOnD9C2NFIprc770lhllS/1MKBDr0GdQ9NXQ==
[email protected].1:
version "5.33.1"
resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.33.1.tgz#d9e7847469d3ffb6e7fd35fbb7b720f2e25d172e"
integrity sha512-TCDmChJe07cYyL3sErc6qycRFMA+HbflCKRGrFvVvpU0RdWJljaqiOo3avFSauciSnQxx9WxzTkMism8YsFHcQ==
dependencies:
"@gorhom/bottom-sheet" "4.4.8"
dayjs "1.10.5"
Expand Down
4 changes: 4 additions & 0 deletions package/jest-global-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/* eslint-disable require-await */
module.exports = async () => {
process.env.TZ = 'UTC';
};
1 change: 1 addition & 0 deletions package/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* global require */
// eslint-disable-next-line no-undef
module.exports = {
globalSetup: './jest-global-setup.js',
moduleNameMapper: {
'mock-builders(.*)$': '<rootDir>/src/mock-builders$1',
},
Expand Down
2 changes: 1 addition & 1 deletion package/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
"eslint-plugin-typescript-sort-keys": "3.2.0",
"i18next-parser": "^9.0.0",
"jest": "29.6.3",
"moment": "2.29.2",
"moment-timezone": "^0.5.45",
"prettier": "2.8.8",
"react": "18.2.0",
"react-docgen-typescript": "1.22.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { useContext } from 'react';
import Dayjs from 'dayjs';

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

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

Expand Down
70 changes: 53 additions & 17 deletions package/src/utils/__tests__/Streami18n.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { default as Dayjs } from 'dayjs';
import 'dayjs/locale/nl';
import localeData from 'dayjs/plugin/localeData';
import moment from 'moment-timezone';

import frTranslations from '../../i18n/fr.json';
import nlTranslations from '../../i18n/nl.json';
Expand All @@ -18,6 +19,12 @@ const customDayjsLocaleConfig = {
weekdaysShort: 'sun_mán_týs_mik_hós_frí_ley'.split('_'),
};

describe('Jest Timezone', () => {
it('global config should set the timezone to UTC', () => {
expect(new Date().getTimezoneOffset()).toBe(0);
});
});

describe('Streami18n instance - default', () => {
const streami18nOptions = { logger: () => {} };
const streami18n = new Streami18n(streami18nOptions);
Expand Down Expand Up @@ -184,24 +191,53 @@ 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 }}' },
describe('Streami18n timezone', () => {
describe.each([['moment', moment]])('%s', (moduleName, module) => {
it('is by default the local timezone', () => {
const streamI18n = new Streami18n({ DateTimeParser: module });
const date = new Date();
expect(streamI18n.tDateTimeParser(date).format('H')).toBe(date.getHours().toString());
});
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 }}' },

it('can be set to different timezone on init', () => {
const streamI18n = new Streami18n({ DateTimeParser: module, timezone: 'Europe/Prague' });
const date = new Date();
expect(streamI18n.tDateTimeParser(date).format('H')).not.toBe(date.getHours().toString());
expect(streamI18n.tDateTimeParser(date).format('H')).not.toBe(
(date.getUTCHours() - 2).toString(),
);
});

it('is ignored if datetime parser does not support timezones', () => {
const tz = module.tz;
delete module.tz;

const streamI18n = new Streami18n({ DateTimeParser: module, timezone: 'Europe/Prague' });
const date = new Date();
expect(streamI18n.tDateTimeParser(date).format('H')).toBe(date.getHours().toString());

module.tz = tz;
});
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');
});
});
await i18n.init();
expect(i18n.t('abc')).toBe('custom');
});
});
49 changes: 33 additions & 16 deletions package/src/utils/i18n/Streami18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import localeData from 'dayjs/plugin/localeData';
import LocalizedFormat from 'dayjs/plugin/localizedFormat';
import relativeTime from 'dayjs/plugin/relativeTime';
import updateLocale from 'dayjs/plugin/updateLocale';
import utc from 'dayjs/plugin/utc';
import i18n, { FallbackLng, TFunction } from 'i18next';

import type moment from 'moment';
import type momentTimezone from 'moment-timezone';

import { calendarFormats } from './calendarFormats';
import {
Expand Down Expand Up @@ -54,6 +55,7 @@ const defaultNS = 'translation';
const defaultLng = 'en';

Dayjs.extend(updateLocale);
Dayjs.extend(utc);

Dayjs.updateLocale('en', {
calendar: calendarFormats.en,
Expand Down Expand Up @@ -147,18 +149,28 @@ const en_locale = {
weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
};

type DateTimeParserModule = typeof Dayjs | typeof momentTimezone;

// Type guards to check DayJs
const isDayJs = (dateTimeParser: typeof Dayjs | typeof moment): dateTimeParser is typeof Dayjs =>
const isDayJs = (dateTimeParser: DateTimeParserModule): dateTimeParser is typeof Dayjs =>
(dateTimeParser as typeof Dayjs).extend !== undefined;

type TimezoneParser = {
tz: momentTimezone.MomentTimezone | Dayjs.Dayjs;
};

const supportsTz = (dateTimeParser: unknown): dateTimeParser is TimezoneParser =>
(dateTimeParser as TimezoneParser).tz !== undefined;

type Streami18nOptions = {
DateTimeParser?: typeof Dayjs | typeof moment;
DateTimeParser?: DateTimeParserModule;
dayjsLocaleConfigForLanguage?: Partial<ILocale>;
debug?: boolean;
disableDateTimeTranslations?: boolean;
formatters?: Partial<PredefinedFormatters> & CustomFormatters;
language?: string;
logger?: (msg?: string) => void;
timezone?: string;
translationsForLanguage?: Partial<typeof enTranslations>;
};

Expand Down Expand Up @@ -385,10 +397,14 @@ export class Streami18n {
*/
logger: (msg?: string) => void;
currentLanguage: string;
DateTimeParser: typeof Dayjs | typeof moment;
DateTimeParser: DateTimeParserModule;
formatters: PredefinedFormatters & CustomFormatters = predefinedFormatters;
isCustomDateTimeParser: boolean;
i18nextConfig: I18NextConfig;
/**
* A valid TZ identifier string (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
*/
timezone?: string;

/**
* Constructor accepts following options:
Expand Down Expand Up @@ -427,6 +443,7 @@ export class Streami18n {

this.currentLanguage = finalOptions.language;
this.DateTimeParser = finalOptions.DateTimeParser;
this.timezone = finalOptions.timezone;
this.formatters = { ...predefinedFormatters, ...options?.formatters };

try {
Expand Down Expand Up @@ -504,19 +521,19 @@ export class Streami18n {
}

this.tDateTimeParser = (timestamp) => {
if (finalOptions.disableDateTimeTranslations || !this.localeExists(this.currentLanguage)) {
/**
* TS needs to know which is being called to accept the chain call
*/
if (isDayJs(this.DateTimeParser)) {
return this.DateTimeParser(timestamp).locale(defaultLng);
}
return this.DateTimeParser(timestamp).locale(defaultLng);
const language =
finalOptions.disableDateTimeTranslations || !this.localeExists(this.currentLanguage)
? defaultLng
: this.currentLanguage;

// If the DateTimeParser is not a Dayjs instance, we assume it is a Moment instance.
if (!isDayJs(this.DateTimeParser)) {
return supportsTz(this.DateTimeParser) && this.timezone
? this.DateTimeParser(timestamp).tz(this.timezone).locale(language)
: this.DateTimeParser(timestamp).locale(language);
}
if (isDayJs(this.DateTimeParser)) {
return this.DateTimeParser(timestamp).locale(this.currentLanguage);
}
return this.DateTimeParser(timestamp).locale(this.currentLanguage);

return this.DateTimeParser(timestamp).locale(language);
};
}

Expand Down
15 changes: 11 additions & 4 deletions package/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8765,10 +8765,17 @@ mktemp@~0.4.0:
resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b"
integrity sha512-IXnMcJ6ZyTuhRmJSjzvHSRhlVPiN9Jwc6e59V0bEJ0ba6OBeX2L0E+mRN1QseeOF4mM+F1Rit6Nh7o+rl2Yn/A==

[email protected]:
version "2.29.2"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4"
integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==
moment-timezone@^0.5.45:
version "0.5.45"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.45.tgz#cb685acd56bac10e69d93c536366eb65aa6bcf5c"
integrity sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==
dependencies:
moment "^2.29.4"

moment@^2.29.4:
version "2.30.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae"
integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==

move-concurrently@^1.0.1:
version "1.0.1"
Expand Down

0 comments on commit a7387a2

Please sign in to comment.