Skip to content

Commit

Permalink
feat: useNow
Browse files Browse the repository at this point in the history
  • Loading branch information
jungpaeng committed Oct 2, 2023
1 parent 5b17cf4 commit 62d1ad4
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 18 deletions.
1 change: 1 addition & 0 deletions packages/core/src/intl.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { type IntlMessage } from './types/intl-message';
export type IntlContextValue = {
message?: IntlMessage;
locale: string;
now?: Date;
timeZone?: string;
formats?: Partial<Format>;
onError: (error: IntlError) => void;
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/intl.provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function defaultGetMessageFallback({ key, namespace }: DefaultGetMessageFallback
}

type IntlProviderProps = React.PropsWithChildren<
Pick<IntlContextValue, 'message' | 'locale' | 'formats' | 'timeZone'> &
Pick<IntlContextValue, 'message' | 'locale' | 'formats' | 'now' | 'timeZone'> &
Partial<Pick<IntlContextValue, 'onError' | 'getMessageFallback'>>
>;

Expand Down
26 changes: 19 additions & 7 deletions packages/core/src/use-intl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function getRelativeTimeFormatConfig(seconds: number) {
}

export function useIntl() {
const { formats, locale, timeZone, onError } = useIntlContext();
const { formats, locale, now: globalNow, timeZone, onError } = useIntlContext();

function resolveFormatOrOptions<Format>(
typeFormats: Record<string, Format> | undefined,
Expand Down Expand Up @@ -120,14 +120,26 @@ export function useIntl() {
});
}

function formatRelativeTime(date: number | Date, now: number | Date) {
const dateDate = date instanceof Date ? date : new Date(date);
const nowDate = now instanceof Date ? now : new Date(now);
function formatRelativeTime(date: number | Date, now?: number | Date) {
try {
if (now == null) {
if (globalNow != null) {
now = globalNow;
} else {
throw new Error(
process.env.NODE_ENV !== 'production'
? `The \`now\` parameter wasn't provided to \`formatRelativeTime\` and there was no global fallback configured on the provider.`
: undefined,
);
}
}

const seconds = (dateDate.getTime() - nowDate.getTime()) / 1_000;
const { unit, value } = getRelativeTimeFormatConfig(seconds);
const dateDate = date instanceof Date ? date : new Date(date);
const nowDate = now instanceof Date ? now : new Date(now);

const seconds = (dateDate.getTime() - nowDate.getTime()) / 1_000;
const { unit, value } = getRelativeTimeFormatConfig(seconds);

try {
return new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(value, unit);
} catch (_error) {
const error = _error as SystemError;
Expand Down
30 changes: 30 additions & 0 deletions packages/core/src/use-now.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';

import { useIntlContext } from './intl.provider';

type UseNowOptions = {
updateInterval?: number;
};

export function useNow(options?: UseNowOptions) {
const updateInterval = options?.updateInterval;

const { now: globalNow } = useIntlContext();
const [now, setNow] = React.useState(globalNow || new Date());

React.useEffect(() => {
if (Boolean(updateInterval) === false) {
return undefined;
}

const intervalId = setInterval(() => {
setNow(new Date());
}, updateInterval);

return () => {
clearInterval(intervalId);
};
}, [updateInterval]);

return now;
}
58 changes: 48 additions & 10 deletions packages/core/test/use-intl.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('formatDateTime', () => {
}

render(
<IntlProvider message={{}} locale="en">
<IntlProvider locale="en">
<Component />
</IntlProvider>,
);
Expand Down Expand Up @@ -91,7 +91,7 @@ describe('formatDateTime', () => {
}

render(
<IntlProvider timeZone="Asia/Seoul" message={{}} locale={'en'}>
<IntlProvider timeZone="Asia/Seoul" locale={'en'}>
<Component />
</IntlProvider>,
);
Expand All @@ -114,7 +114,7 @@ describe('formatDateTime', () => {
}

render(
<IntlProvider timeZone="Asia/Seoul" message={{}} locale={'en'}>
<IntlProvider timeZone="Asia/Seoul" locale={'en'}>
<Component />
</IntlProvider>,
);
Expand All @@ -123,6 +123,22 @@ describe('formatDateTime', () => {
});
});

it('can use a global `now` fallback', () => {
function Component() {
const intl = useIntl();
const mockDate = new Date('1984-11-20T10:36:00.000Z');
return <>{intl.formatRelativeTime(mockDate)}</>;
}

render(
<IntlProvider now={new Date('2018-11-20T10:36:00.000Z')} locale={'en'}>
<Component />
</IntlProvider>,
);

screen.getByText('34 years ago');
});

describe('error handling', () => {
it('handles missing formats', () => {
const onError = vi.fn();
Expand All @@ -133,7 +149,7 @@ describe('formatDateTime', () => {
}

const { container } = render(
<IntlProvider onError={onError} message={{}} locale={'en'}>
<IntlProvider onError={onError} locale={'en'}>
<Component />
</IntlProvider>,
);
Expand All @@ -155,7 +171,7 @@ describe('formatDateTime', () => {
}

const { container } = render(
<IntlProvider onError={onError} message={{}} locale={'en'}>
<IntlProvider onError={onError} locale={'en'}>
<Component />
</IntlProvider>,
);
Expand All @@ -167,6 +183,28 @@ describe('formatDateTime', () => {
);
expect(container.textContent).toMatch(/Oct 01 2023/);
});

it('throws when no `now` value is available', () => {
const onError = vi.fn();

function Component() {
const intl = useIntl();
const mockDate = new Date('1984-11-20T10:36:00.000Z');
return <>{intl.formatRelativeTime(mockDate)}</>;
}

render(
<IntlProvider onError={onError} locale={'en'}>
<Component />
</IntlProvider>,
);

const error: IntlError = onError.mock.calls[0][0];
expect(error.code).toBe(IntlErrorCode.FORMATTING_ERROR);
expect(error.message).toBe(
"FORMATTING_ERROR: The `now` parameter wasn't provided to `formatRelativeTime` and there was no global fallback configured on the provider.",
);
});
});
});

Expand All @@ -178,7 +216,7 @@ describe('formatNumber', () => {
}

render(
<IntlProvider message={{}} locale="en">
<IntlProvider locale="en">
<Component />
</IntlProvider>,
);
Expand Down Expand Up @@ -225,7 +263,7 @@ describe('formatNumber', () => {
}

const { container } = render(
<IntlProvider onError={onError} message={{}} locale={'en'}>
<IntlProvider onError={onError} locale={'en'}>
<Component />
</IntlProvider>,
);
Expand All @@ -247,7 +285,7 @@ describe('formatNumber', () => {
}

const { container } = render(
<IntlProvider onError={onError} message={{}} locale={'en'}>
<IntlProvider onError={onError} locale={'en'}>
<Component />
</IntlProvider>,
);
Expand All @@ -268,7 +306,7 @@ describe('formatRelativeTime', () => {
}

render(
<IntlProvider message={{}} locale="en">
<IntlProvider locale="en">
<Component />
</IntlProvider>,
);
Expand Down Expand Up @@ -332,7 +370,7 @@ describe('formatRelativeTime', () => {
}

const { container } = render(
<IntlProvider onError={onError} message={{}} locale={'en'}>
<IntlProvider onError={onError} locale={'en'}>
<Component />
</IntlProvider>,
);
Expand Down
72 changes: 72 additions & 0 deletions packages/core/test/use-now.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { render, waitFor } from '@testing-library/react';
import { expect, it } from 'vitest';

import { IntlProvider } from '../src/intl.provider';
import { useNow } from '../src/use-now';

it('returns the current time', () => {
function Component() {
return <p>{useNow().toISOString()}</p>;
}

const { container } = render(
<IntlProvider locale="en">
<Component />
</IntlProvider>,
);
expect(container.textContent).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/);
});

it('can use a globally defined `now` value', () => {
function Component() {
return <p>{useNow().toISOString()}</p>;
}

const { container } = render(
<IntlProvider locale="en" now={new Date('2018-10-06T10:36:01.516Z')}>
<Component />
</IntlProvider>,
);
expect(container.textContent).toBe('2018-10-06T10:36:01.516Z');
});

it('can update a globally defined `now` value after the initial render', async () => {
function Component() {
return <p>{useNow({ updateInterval: 100 }).toISOString()}</p>;
}

const { container } = render(
<IntlProvider locale="en" now={new Date('2018-10-06T10:36:01.516Z')}>
<Component />
</IntlProvider>,
);
expect(container.textContent).toBe('2018-10-06T10:36:01.516Z');

await waitFor(() => {
if (!container.textContent) throw new Error();
const curYear = parseInt(container.textContent);

expect(curYear).toBeGreaterThan(2020);
});
});

it('can update based on an interval', async () => {
function Component() {
return <p>{useNow({ updateInterval: 100 }).toISOString()}</p>;
}

const { container } = render(
<IntlProvider locale="en">
<Component />
</IntlProvider>,
);
if (!container.textContent) throw new Error();
const initial = new Date(container.textContent).getTime();

await waitFor(() => {
if (!container.textContent) throw new Error();
const now = new Date(container.textContent).getTime();
expect(now - 900).toBeGreaterThanOrEqual(initial);
expect(4).toBeGreaterThanOrEqual(3);
});
});

0 comments on commit 62d1ad4

Please sign in to comment.