Skip to content

Commit 3d4bdf9

Browse files
authored
feat: add timezone support to datetime parsing (#2099)
1 parent 852490d commit 3d4bdf9

File tree

6 files changed

+99
-25
lines changed

6 files changed

+99
-25
lines changed

docusaurus/docs/React/guides/theming/translations.mdx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,28 @@ const i18n = new Streami18n({
286286
287287
If you would like to stick with english language for dates and times in Stream components, you can set `disableDateTimeTranslations` to true.
288288
289+
### Timezone location
290+
291+
To display date and time in different than machine's local timezone, 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.
292+
293+
```ts
294+
import { Streami18n } from 'stream-chat-react';
295+
296+
const streamI18n = new Streami18n({ timezone: 'Europe/Prague'});
297+
```
298+
299+
If you are using `moment` as your datetime parser engine and want to start using timezone-located datetime strings, then we recommend to use `moment-timezone` instead of `moment` package. 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.
300+
301+
```ts
302+
import type momentTimezone from 'moment-timezone';
303+
import { Streami18n } from 'stream-chat-react';
304+
305+
const i18n = new Streami18n({
306+
DateTimeParser: momentTimezone,
307+
timezone: 'Europe/Prague',
308+
})
309+
```
310+
289311
### Translating Messages
290312
291313
Stream Chat provide the ability to run users' messages through automatic translation.
@@ -317,8 +339,7 @@ The `Streami18n` class wraps [`i18next`](https://www.npmjs.com/package/i18next)
317339
| logger | logs warnings/errors | function | () => {} |
318340
| dayjsLocaleConfigForLanguage | internal Day.js [config object](https://github.com/iamkun/dayjs/tree/dev/src/locale) and [calendar locale config object](https://day.js.org/docs/en/plugin/calendar) | object | 'enConfig' |
319341
| DateTimeParser | custom date time parser | function | Day.js |
320-
321-
####
342+
| timezone | valid timezone identifier string (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) | function | Day.js |
322343
323344
### Class Instance Methods
324345

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@
153153
"i18next-parser": "^6.0.0",
154154
"jest": "^26.6.3",
155155
"jest-axe": "^6.0.0",
156-
"moment": "^2.29.1",
156+
"moment-timezone": "^0.5.43",
157157
"postcss": "^8.1.10",
158158
"postcss-loader": "^4.1.0",
159159
"prettier": "^2.2.0",

src/context/TranslationContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import localizedFormat from 'dayjs/plugin/localizedFormat';
66
import { getDisplayName } from './utils/getDisplayName';
77

88
import type { TFunction } from 'i18next';
9-
import type { Moment } from 'moment';
9+
import type { Moment } from 'moment-timezone';
1010
import type { TranslationLanguages } from 'stream-chat';
1111

1212
import type { UnknownType } from '../types/types';

src/i18n/Streami18n.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import updateLocale from 'dayjs/plugin/updateLocale';
55
import LocalizedFormat from 'dayjs/plugin/localizedFormat';
66
import localeData from 'dayjs/plugin/localeData';
77
import relativeTime from 'dayjs/plugin/relativeTime';
8+
import utc from 'dayjs/plugin/utc';
9+
import timezone from 'dayjs/plugin/timezone';
810

9-
import type moment from 'moment';
11+
import type momentTimezone from 'moment-timezone';
1012
import type { TranslationLanguages } from 'stream-chat';
1113

1214
import type { TDateTimeParser } from '../context/TranslationContext';
@@ -57,6 +59,8 @@ type CalendarLocaleConfig = {
5759
};
5860

5961
Dayjs.extend(updateLocale);
62+
Dayjs.extend(utc);
63+
Dayjs.extend(timezone);
6064

6165
Dayjs.updateLocale('de', {
6266
calendar: {
@@ -227,17 +231,25 @@ const en_locale = {
227231
weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
228232
};
229233

234+
type DateTimeParserModule = typeof Dayjs | typeof momentTimezone;
230235
// Type guards to check DayJs
231-
const isDayJs = (dateTimeParser: typeof Dayjs | typeof moment): dateTimeParser is typeof Dayjs =>
236+
const isDayJs = (dateTimeParser: DateTimeParserModule): dateTimeParser is typeof Dayjs =>
232237
(dateTimeParser as typeof Dayjs).extend !== undefined;
233238

239+
type TimezoneParser = {
240+
tz: momentTimezone.MomentTimezone | Dayjs.Dayjs;
241+
};
242+
const supportsTz = (dateTimeParser: unknown): dateTimeParser is TimezoneParser =>
243+
(dateTimeParser as TimezoneParser).tz !== undefined;
244+
234245
type Options = {
235-
DateTimeParser?: typeof Dayjs | typeof moment;
246+
DateTimeParser?: DateTimeParserModule;
236247
dayjsLocaleConfigForLanguage?: Partial<ILocale> & { calendar?: CalendarLocaleConfig };
237248
debug?: boolean;
238249
disableDateTimeTranslations?: boolean;
239250
language?: TranslationLanguages;
240251
logger?: (message?: string) => void;
252+
timezone?: string;
241253
translationsForLanguage?: Partial<typeof enTranslations>;
242254
};
243255

@@ -446,7 +458,7 @@ export class Streami18n {
446458
*/
447459
logger: (msg?: string) => void;
448460
currentLanguage: TranslationLanguages;
449-
DateTimeParser: typeof Dayjs | typeof moment;
461+
DateTimeParser: DateTimeParserModule;
450462
isCustomDateTimeParser: boolean;
451463
i18nextConfig: {
452464
debug: boolean;
@@ -457,6 +469,10 @@ export class Streami18n {
457469
nsSeparator: false;
458470
parseMissingKeyHandler: (key: string) => string;
459471
};
472+
/**
473+
* A valid TZ identifier string (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
474+
*/
475+
timezone?: string;
460476
/**
461477
* Constructor accepts following options:
462478
* - language (String) default: 'en'
@@ -492,6 +508,7 @@ export class Streami18n {
492508
this.logger = finalOptions.logger;
493509
this.currentLanguage = finalOptions.language;
494510
this.DateTimeParser = finalOptions.DateTimeParser;
511+
this.timezone = finalOptions.timezone;
495512

496513
try {
497514
if (this.DateTimeParser && isDayJs(this.DateTimeParser)) {
@@ -561,21 +578,21 @@ export class Streami18n {
561578
}
562579

563580
this.tDateTimeParser = (timestamp) => {
564-
if (finalOptions.disableDateTimeTranslations || !this.localeExists(this.currentLanguage)) {
565-
/**
566-
* TS needs to know which is being called to accept the chain call
567-
*/
568-
if (isDayJs(this.DateTimeParser)) {
569-
return this.DateTimeParser(timestamp).locale(defaultLng);
570-
}
571-
return this.DateTimeParser(timestamp).locale(defaultLng);
572-
}
581+
const language =
582+
finalOptions.disableDateTimeTranslations || !this.localeExists(this.currentLanguage)
583+
? defaultLng
584+
: this.currentLanguage;
573585

574586
if (isDayJs(this.DateTimeParser)) {
575-
return this.DateTimeParser(timestamp).locale(this.currentLanguage);
587+
return supportsTz(this.DateTimeParser)
588+
? this.DateTimeParser(timestamp).tz(this.timezone).locale(language)
589+
: this.DateTimeParser(timestamp).locale(language);
576590
}
577591

578-
return this.DateTimeParser(timestamp).locale(this.currentLanguage);
592+
if (supportsTz(this.DateTimeParser) && this.timezone) {
593+
return this.DateTimeParser(timestamp).tz(this.timezone).locale(language);
594+
}
595+
return this.DateTimeParser(timestamp).locale(language);
579596
};
580597
}
581598

src/i18n/__tests__/Streami18n.test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { Streami18n } from '../Streami18n';
33
import { nanoid } from 'nanoid';
44
import { default as Dayjs } from 'dayjs';
5+
import moment from 'moment-timezone';
56
import { nlTranslations, frTranslations } from '../translations';
67
import 'dayjs/locale/nl';
78
import localeData from 'dayjs/plugin/localeData';
@@ -226,3 +227,36 @@ describe('setLanguage - switch to french', () => {
226227
}
227228
});
228229
});
230+
231+
describe('Streami18n timezone', () => {
232+
describe.each([
233+
['Dayjs', Dayjs],
234+
['moment', moment],
235+
])('%s', (moduleName, module) => {
236+
it('is by default the local timezone', () => {
237+
const streamI18n = new Streami18n({ DateTimeParser: module });
238+
const date = new Date();
239+
expect(streamI18n.tDateTimeParser(date).format('H')).toBe(date.getHours().toString());
240+
});
241+
242+
it('can be set to different timezone on init', () => {
243+
const streamI18n = new Streami18n({ DateTimeParser: module, timezone: 'Europe/Prague' });
244+
const date = new Date();
245+
expect(streamI18n.tDateTimeParser(date).format('H')).not.toBe(date.getHours().toString());
246+
expect(streamI18n.tDateTimeParser(date).format('H')).not.toBe(
247+
(date.getUTCHours() - 2).toString(),
248+
);
249+
});
250+
251+
it('is ignored if datetime parser does not support timezones', () => {
252+
const tz = module.tz;
253+
delete module.tz;
254+
255+
const streamI18n = new Streami18n({ DateTimeParser: module, timezone: 'Europe/Prague' });
256+
const date = new Date();
257+
expect(streamI18n.tDateTimeParser(date).format('H')).toBe(date.getHours().toString());
258+
259+
module.tz = tz;
260+
});
261+
});
262+
});

yarn.lock

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10465,16 +10465,18 @@ modify-values@^1.0.0:
1046510465
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
1046610466
integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==
1046710467

10468-
moment@*:
10468+
moment-timezone@^0.5.43:
10469+
version "0.5.43"
10470+
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.43.tgz#3dd7f3d0c67f78c23cd1906b9b2137a09b3c4790"
10471+
integrity sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==
10472+
dependencies:
10473+
moment "^2.29.4"
10474+
10475+
moment@*, moment@^2.29.4:
1046910476
version "2.29.4"
1047010477
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
1047110478
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
1047210479

10473-
moment@^2.29.1:
10474-
version "2.29.2"
10475-
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4"
10476-
integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==
10477-
1047810480
move-concurrently@^1.0.1:
1047910481
version "1.0.1"
1048010482
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"

0 commit comments

Comments
 (0)