@jobber/components-native 0.45.1 → 0.46.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/package.json +2 -2
  2. package/dist/src/AtlantisContext/AtlantisContext.js +2 -2
  3. package/dist/src/Form/context/AtlantisFormContext.js +2 -2
  4. package/dist/src/InputDate/InputDate.js +3 -8
  5. package/dist/src/InputText/context/InputAccessoriesContext.js +2 -2
  6. package/dist/src/InputTime/InputTime.js +3 -6
  7. package/dist/src/hooks/useAtlantisI18n/useAtlantisI18n.js +7 -3
  8. package/dist/src/hooks/useAtlantisI18n/utils/dateFormatter.js +20 -0
  9. package/dist/tsconfig.tsbuildinfo +1 -1
  10. package/dist/types/src/AtlantisContext/AtlantisContext.d.ts +1 -1
  11. package/dist/types/src/Form/context/AtlantisFormContext.d.ts +1 -1
  12. package/dist/types/src/hooks/useAtlantisI18n/useAtlantisI18n.d.ts +17 -0
  13. package/dist/types/src/hooks/useAtlantisI18n/utils/dateFormatter.d.ts +6 -0
  14. package/package.json +2 -2
  15. package/src/AtlantisContext/AtlantisContext.test.tsx +3 -2
  16. package/src/AtlantisContext/AtlantisContext.tsx +2 -2
  17. package/src/ButtonGroup/ButtonGroup.test.tsx +2 -2
  18. package/src/Form/Form.test.tsx +2 -2
  19. package/src/Form/components/FormErrorBanner/FormErrorBanner.test.tsx +2 -2
  20. package/src/Form/context/AtlantisFormContext.test.tsx +2 -3
  21. package/src/Form/context/AtlantisFormContext.tsx +4 -3
  22. package/src/InputCurrency/InputCurrency.test.tsx +2 -2
  23. package/src/InputDate/InputDate.test.tsx +4 -4
  24. package/src/InputDate/InputDate.tsx +3 -8
  25. package/src/InputText/context/InputAccessoriesContext.ts +4 -2
  26. package/src/InputTime/InputTime.test.tsx +3 -3
  27. package/src/InputTime/InputTime.tsx +4 -7
  28. package/src/hooks/useAtlantisI18n/useAtlantisI18n.test.ts +105 -4
  29. package/src/hooks/useAtlantisI18n/useAtlantisI18n.ts +40 -4
  30. package/src/hooks/useAtlantisI18n/utils/dateFormatter.ts +31 -0
@@ -51,6 +51,6 @@ export interface AtlantisContextProps {
51
51
  */
52
52
  readonly setHeaderHeight: (height: number) => void;
53
53
  }
54
- export declare const defaultValues: AtlantisContextProps;
54
+ export declare const atlantisContextDefaultValues: AtlantisContextProps;
55
55
  export declare const AtlantisContext: import("react").Context<AtlantisContextProps>;
56
56
  export declare function useAtlantisContext(): AtlantisContextProps;
@@ -1,6 +1,6 @@
1
1
  /// <reference types="react" />
2
2
  import { AtlantisFormContextProps } from "./types";
3
- export declare const defaultValues: {
3
+ export declare const atlantisFormContextDefaultValues: {
4
4
  useConfirmBeforeBack: () => import("react").MutableRefObject<() => undefined>;
5
5
  useInternalFormLocalCache: () => {
6
6
  setLocalCache: () => undefined;
@@ -1,7 +1,24 @@
1
1
  import en from "./locales/en.json";
2
2
  export type I18nKeys = keyof typeof en;
3
3
  export interface useAtlantisI18nValue {
4
+ /**
5
+ * The set locale based on the AtlantisContext.
6
+ */
4
7
  readonly locale: string;
8
+ /**
9
+ * Returns the translated string depending on the locale. This accepts a 2nd
10
+ * param for string interpolation.
11
+ */
5
12
  readonly t: (message: I18nKeys, values?: Record<string, string>) => string;
13
+ /**
14
+ * Returns a formatted date string based on the locale and the `dateFormat`
15
+ * set in AtlantisContext.
16
+ */
17
+ readonly formatDate: (date: Date) => string;
18
+ /**
19
+ * Returns a formatted time string based on the locale and the `timeFormat`
20
+ * set in AtlantisContext.
21
+ */
22
+ readonly formatTime: (date: Date) => string;
6
23
  }
7
24
  export declare function useAtlantisI18n(): useAtlantisI18nValue;
@@ -0,0 +1,6 @@
1
+ interface DateFormatterOptions {
2
+ readonly locale: string;
3
+ readonly timeZone: string;
4
+ }
5
+ export declare function dateFormatter(date: Date, dateTimeFormat: string, { locale, timeZone }: DateFormatterOptions): string;
6
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components-native",
3
- "version": "0.45.1",
3
+ "version": "0.46.1",
4
4
  "license": "MIT",
5
5
  "description": "React Native implementation of Atlantis",
6
6
  "repository": {
@@ -84,5 +84,5 @@
84
84
  "react-native-reanimated": "^2.17.0",
85
85
  "react-native-safe-area-context": "^4.5.2"
86
86
  },
87
- "gitHead": "8764198c93cfc2812f504b6f6509595ad6b3195c"
87
+ "gitHead": "ebd19569f57d6c7ef08de4992894952452dfe583"
88
88
  }
@@ -4,7 +4,7 @@ import { cleanup, renderHook } from "@testing-library/react-hooks";
4
4
  import {
5
5
  AtlantisContext,
6
6
  AtlantisContextProps,
7
- defaultValues,
7
+ atlantisContextDefaultValues,
8
8
  useAtlantisContext,
9
9
  } from "./AtlantisContext";
10
10
 
@@ -14,6 +14,7 @@ const providerValues: AtlantisContextProps = {
14
14
  dateFormat: "MM/DD/YYYY",
15
15
  timeFormat: "hh:mm a",
16
16
  timeZone: "America/Edmonton",
17
+ locale: "en",
17
18
  isOnline: false,
18
19
  onLogError: _ => {
19
20
  return;
@@ -31,7 +32,7 @@ describe("AtlantisContext", () => {
31
32
  it("should get the default values", () => {
32
33
  const { result } = renderHook(() => useAtlantisContext());
33
34
 
34
- expect(result.current).toMatchObject(defaultValues);
35
+ expect(result.current).toMatchObject(atlantisContextDefaultValues);
35
36
  });
36
37
  });
37
38
 
@@ -64,7 +64,7 @@ export interface AtlantisContextProps {
64
64
  readonly setHeaderHeight: (height: number) => void;
65
65
  }
66
66
 
67
- export const defaultValues: AtlantisContextProps = {
67
+ export const atlantisContextDefaultValues: AtlantisContextProps = {
68
68
  dateFormat: "PP",
69
69
  // The system time is "p"
70
70
  timeFormat: "p",
@@ -82,7 +82,7 @@ export const defaultValues: AtlantisContextProps = {
82
82
  },
83
83
  };
84
84
 
85
- export const AtlantisContext = createContext(defaultValues);
85
+ export const AtlantisContext = createContext(atlantisContextDefaultValues);
86
86
 
87
87
  export function useAtlantisContext(): AtlantisContextProps {
88
88
  return useContext(AtlantisContext);
@@ -5,7 +5,7 @@ import { Alert } from "react-native";
5
5
  import { ButtonGroup, ButtonGroupProps } from "./ButtonGroup";
6
6
  import { Button } from "../Button";
7
7
  import * as atlantisContext from "../AtlantisContext/AtlantisContext";
8
- import { defaultValues as contextDefaultValue } from "../AtlantisContext";
8
+ import { atlantisContextDefaultValues } from "../AtlantisContext";
9
9
 
10
10
  const mockOnOpen = jest.fn();
11
11
 
@@ -300,7 +300,7 @@ describe("ButtonGroup Offline/Online", () => {
300
300
  it("should show an alert and not fire the onPress", () => {
301
301
  const alertSpy = jest.spyOn(Alert, "alert");
302
302
  atlantisContextSpy.mockReturnValue({
303
- ...contextDefaultValue,
303
+ ...atlantisContextDefaultValues,
304
304
  isOnline: false,
305
305
  });
306
306
 
@@ -4,7 +4,7 @@ import { Alert, Keyboard } from "react-native";
4
4
  import { Host } from "react-native-portalize";
5
5
  import { Form, FormBannerMessage, FormBannerMessageType } from ".";
6
6
  import { FormBannerErrors, FormSubmitErrorType } from "./types";
7
- import { defaultValues as contextDefaultValue } from "../AtlantisContext";
7
+ import { atlantisContextDefaultValues } from "../AtlantisContext";
8
8
  import * as atlantisContext from "../AtlantisContext/AtlantisContext";
9
9
  import { Text } from "../Text";
10
10
  import { Checkbox } from "../Checkbox";
@@ -476,7 +476,7 @@ describe("Form", () => {
476
476
  );
477
477
 
478
478
  atlantisContextSpy.mockReturnValue({
479
- ...contextDefaultValue,
479
+ ...atlantisContextDefaultValues,
480
480
  isOnline: false,
481
481
  });
482
482
 
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import { cleanup, render } from "@testing-library/react-native";
3
3
  import { FormErrorBanner } from "./FormErrorBanner";
4
- import { defaultValues as contextDefaultValue } from "../../../AtlantisContext";
4
+ import { atlantisContextDefaultValues } from "../../../AtlantisContext";
5
5
  import * as atlantisContext from "../../../AtlantisContext/AtlantisContext";
6
6
 
7
7
  describe("FormErrorBanner", () => {
@@ -9,7 +9,7 @@ describe("FormErrorBanner", () => {
9
9
 
10
10
  beforeEach(() => {
11
11
  atlantisContextSpy.mockReturnValue({
12
- ...contextDefaultValue,
12
+ ...atlantisContextDefaultValues,
13
13
  isOnline: true,
14
14
  });
15
15
  });
@@ -3,7 +3,7 @@ import { renderHook } from "@testing-library/react-hooks";
3
3
  import { AtlantisFormContextProps } from "./types";
4
4
  import {
5
5
  AtlantisFormContext,
6
- defaultValues,
6
+ atlantisFormContextDefaultValues,
7
7
  useAtlantisFormContext,
8
8
  } from "./AtlantisFormContext";
9
9
 
@@ -13,7 +13,6 @@ const useInternalFormLocalCacheMock = jest.fn();
13
13
  const providerValues: AtlantisFormContextProps = {
14
14
  useConfirmBeforeBack: useConfirmBeforeBackMock,
15
15
  useInternalFormLocalCache: useInternalFormLocalCacheMock,
16
- headerHeight: 50,
17
16
  };
18
17
 
19
18
  describe("AtlantisFormContext", () => {
@@ -25,7 +24,7 @@ describe("AtlantisFormContext", () => {
25
24
  it("should get the default values", () => {
26
25
  const { result } = renderHook(() => useAtlantisFormContext());
27
26
 
28
- expect(result.current).toMatchObject(defaultValues);
27
+ expect(result.current).toMatchObject(atlantisFormContextDefaultValues);
29
28
  });
30
29
  });
31
30
 
@@ -1,7 +1,7 @@
1
1
  import { createContext, useContext, useRef } from "react";
2
2
  import { AtlantisFormContextProps } from "./types";
3
3
 
4
- export const defaultValues = {
4
+ export const atlantisFormContextDefaultValues = {
5
5
  useConfirmBeforeBack: () => {
6
6
  const ref = useRef(() => undefined);
7
7
  return ref;
@@ -12,8 +12,9 @@ export const defaultValues = {
12
12
  }),
13
13
  };
14
14
 
15
- export const AtlantisFormContext =
16
- createContext<AtlantisFormContextProps>(defaultValues);
15
+ export const AtlantisFormContext = createContext<AtlantisFormContextProps>(
16
+ atlantisFormContextDefaultValues,
17
+ );
17
18
 
18
19
  export function useAtlantisFormContext(): AtlantisFormContextProps {
19
20
  return useContext(AtlantisFormContext);
@@ -4,12 +4,12 @@ import { InputCurrency } from "./InputCurrency";
4
4
  import {
5
5
  AtlantisContext,
6
6
  AtlantisContextProps,
7
- defaultValues,
7
+ atlantisContextDefaultValues,
8
8
  } from "../AtlantisContext";
9
9
 
10
10
  const mockCurrencySymbol = "£";
11
11
  const atlantisContext: AtlantisContextProps = {
12
- ...defaultValues,
12
+ ...atlantisContextDefaultValues,
13
13
  currencySymbol: mockCurrencySymbol,
14
14
  timeFormat: "p",
15
15
  timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
@@ -238,7 +238,7 @@ describe("InputDate", () => {
238
238
 
239
239
  it("should display MM/DD/YYYY when dateFormat is 'P'", () => {
240
240
  jest.spyOn(atlantisContext, "useAtlantisContext").mockReturnValue({
241
- ...atlantisContext.defaultValues,
241
+ ...atlantisContext.atlantisContextDefaultValues,
242
242
  dateFormat: "P",
243
243
  });
244
244
  const expectedDate = "05/24/2023";
@@ -252,7 +252,7 @@ describe("InputDate", () => {
252
252
 
253
253
  it("should display mmmm d, yyyy when dateFormat is 'PP'", () => {
254
254
  jest.spyOn(atlantisContext, "useAtlantisContext").mockReturnValue({
255
- ...atlantisContext.defaultValues,
255
+ ...atlantisContext.atlantisContextDefaultValues,
256
256
  dateFormat: "PP",
257
257
  });
258
258
  const expectedDate = "Feb 20, 2023";
@@ -266,7 +266,7 @@ describe("InputDate", () => {
266
266
 
267
267
  it("should display mmmmm d, yyyy when dateFormat is 'PPP'", () => {
268
268
  jest.spyOn(atlantisContext, "useAtlantisContext").mockReturnValue({
269
- ...atlantisContext.defaultValues,
269
+ ...atlantisContext.atlantisContextDefaultValues,
270
270
  dateFormat: "PPP",
271
271
  });
272
272
  const expectedDate = "July 7th, 2023";
@@ -280,7 +280,7 @@ describe("InputDate", () => {
280
280
 
281
281
  it("should display dddd, mmmmm d, yyyy when dateFormat is 'PPPP'", () => {
282
282
  jest.spyOn(atlantisContext, "useAtlantisContext").mockReturnValue({
283
- ...atlantisContext.defaultValues,
283
+ ...atlantisContext.atlantisContextDefaultValues,
284
284
  dateFormat: "PPPP",
285
285
  });
286
286
  const expectedDate = "Thursday, June 22nd, 2023";
@@ -3,12 +3,9 @@ import DateTimePicker from "react-native-modal-datetime-picker";
3
3
  import { Platform } from "react-native";
4
4
  import { FieldError, UseControllerProps } from "react-hook-form";
5
5
  import { XOR } from "ts-xor";
6
- import { utcToZonedTime } from "date-fns-tz";
7
- import { format as formatDate } from "date-fns";
8
6
  import { Clearable, InputFieldWrapperProps } from "../InputFieldWrapper";
9
7
  import { FormField } from "../FormField";
10
8
  import { InputPressable } from "../InputPressable";
11
- import { useAtlantisContext } from "../AtlantisContext";
12
9
  import { useAtlantisI18n } from "../hooks/useAtlantisI18n";
13
10
 
14
11
  interface BaseInputDateProps
@@ -153,8 +150,7 @@ function InternalInputDate({
153
150
  accessibilityHint,
154
151
  }: InputDateProps): JSX.Element {
155
152
  const [showPicker, setShowPicker] = useState(false);
156
- const { t, locale } = useAtlantisI18n();
157
- const { timeZone, dateFormat } = useAtlantisContext();
153
+ const { t, locale, formatDate } = useAtlantisI18n();
158
154
 
159
155
  const date = useMemo(() => {
160
156
  if (typeof value === "string") return new Date(value);
@@ -163,12 +159,11 @@ function InternalInputDate({
163
159
 
164
160
  const formattedDate = useMemo(() => {
165
161
  if (date) {
166
- const zonedTime = utcToZonedTime(date, timeZone);
167
- return formatDate(zonedTime, dateFormat);
162
+ return formatDate(date);
168
163
  }
169
164
 
170
165
  return emptyValueLabel;
171
- }, [date, emptyValueLabel, timeZone, dateFormat]);
166
+ }, [date, emptyValueLabel]);
172
167
 
173
168
  const canClearDate = formattedDate === emptyValueLabel ? "never" : clearable;
174
169
 
@@ -1,7 +1,7 @@
1
1
  import { createContext, useContext } from "react";
2
2
  import { InputAccessoriesContextProps } from "./types";
3
3
 
4
- const defaultValues: InputAccessoriesContextProps = {
4
+ const inputAccessoriesContextDefaultValues: InputAccessoriesContextProps = {
5
5
  elements: {},
6
6
  focusedInput: "",
7
7
  canFocusNext: false,
@@ -14,7 +14,9 @@ const defaultValues: InputAccessoriesContextProps = {
14
14
  setFocusedInput: () => undefined,
15
15
  };
16
16
 
17
- export const InputAccessoriesContext = createContext(defaultValues);
17
+ export const InputAccessoriesContext = createContext(
18
+ inputAccessoriesContextDefaultValues,
19
+ );
18
20
 
19
21
  export function useInputAccessoriesContext(): InputAccessoriesContextProps {
20
22
  return useContext(InputAccessoriesContext);
@@ -162,7 +162,7 @@ describe("Time picker", () => {
162
162
 
163
163
  it("should be set to 24 hours", () => {
164
164
  jest.spyOn(atlantisContext, "useAtlantisContext").mockReturnValue({
165
- ...atlantisContext.defaultValues,
165
+ ...atlantisContext.atlantisContextDefaultValues,
166
166
  timeZone: "UTC",
167
167
  timeFormat: "HH:mm",
168
168
  });
@@ -295,7 +295,7 @@ describe("Timezone conversion", () => {
295
295
  );
296
296
  it("should display the time in the account timezone", async () => {
297
297
  jest.spyOn(atlantisContext, "useAtlantisContext").mockReturnValue({
298
- ...atlantisContext.defaultValues,
298
+ ...atlantisContext.atlantisContextDefaultValues,
299
299
  timeZone: "America/Los_Angeles",
300
300
  });
301
301
 
@@ -309,7 +309,7 @@ describe("Timezone conversion", () => {
309
309
 
310
310
  it("should have the correct offset on the time picker", async () => {
311
311
  jest.spyOn(atlantisContext, "useAtlantisContext").mockReturnValue({
312
- ...atlantisContext.defaultValues,
312
+ ...atlantisContext.atlantisContextDefaultValues,
313
313
  timeZone: "America/Los_Angeles",
314
314
  });
315
315
 
@@ -3,8 +3,6 @@ import { FieldError, UseControllerProps } from "react-hook-form";
3
3
  import { XOR } from "ts-xor";
4
4
  import DateTimePicker from "react-native-modal-datetime-picker";
5
5
  import { View } from "react-native";
6
- import { utcToZonedTime } from "date-fns-tz";
7
- import { format as formatTime } from "date-fns";
8
6
  import { styles } from "./InputTime.style";
9
7
  import { getTimeZoneOffsetInMinutes, roundUpToNearestMinutes } from "./utils";
10
8
  import { useAtlantisContext } from "../AtlantisContext";
@@ -134,9 +132,9 @@ function InternalInputTime({
134
132
  showIcon = true,
135
133
  }: InputTimeProps): JSX.Element {
136
134
  const [showPicker, setShowPicker] = useState(false);
137
- const { t } = useAtlantisI18n();
138
-
135
+ const { t, formatTime } = useAtlantisI18n();
139
136
  const { timeZone, timeFormat } = useAtlantisContext();
137
+
140
138
  const is24Hour = timeFormat === "HH:mm";
141
139
 
142
140
  const dateTime = useMemo(
@@ -146,12 +144,11 @@ function InternalInputTime({
146
144
 
147
145
  const formattedTime = useMemo(() => {
148
146
  if (dateTime) {
149
- const zonedTime = utcToZonedTime(dateTime, timeZone);
150
- return formatTime(zonedTime, timeFormat);
147
+ return formatTime(dateTime);
151
148
  }
152
149
 
153
150
  return emptyValueLabel;
154
- }, [dateTime, emptyValueLabel, timeZone, timeFormat]);
151
+ }, [dateTime, emptyValueLabel]);
155
152
 
156
153
  const canClearTime = formattedTime === emptyValueLabel ? "never" : clearable;
157
154
 
@@ -10,6 +10,15 @@ jest.mock("../../AtlantisContext", () => ({
10
10
  ...jest.requireActual("../../AtlantisContext"),
11
11
  }));
12
12
 
13
+ const spy = jest.spyOn(context, "useAtlantisContext");
14
+ const testDate = new Date("2020-01-01T00:00:00.000Z");
15
+ const dateAfterSpringForward = new Date("2020-04-10T00:00:00.000Z");
16
+
17
+ beforeEach(() => {
18
+ jest.useFakeTimers();
19
+ jest.setSystemTime(testDate);
20
+ });
21
+
13
22
  describe("useAtlantisI18n", () => {
14
23
  it("should return english by default", () => {
15
24
  const { result } = renderHook(useAtlantisI18n);
@@ -27,8 +36,10 @@ describe("useAtlantisI18n", () => {
27
36
 
28
37
  describe("Español", () => {
29
38
  it("should return español", () => {
30
- const spy = jest.spyOn(context, "useAtlantisContext");
31
- spy.mockReturnValueOnce({ ...context.defaultValues, locale: "es" });
39
+ spy.mockReturnValueOnce({
40
+ ...context.atlantisContextDefaultValues,
41
+ locale: "es",
42
+ });
32
43
  const { result } = renderHook(useAtlantisI18n);
33
44
 
34
45
  expect(result.current.t("cancel")).toBe("Cancelar");
@@ -37,8 +48,10 @@ describe("useAtlantisI18n", () => {
37
48
 
38
49
  describe("Unsupported language", () => {
39
50
  it("should return the english translation", () => {
40
- const spy = jest.spyOn(context, "useAtlantisContext");
41
- spy.mockReturnValueOnce({ ...context.defaultValues, locale: "fr" });
51
+ spy.mockReturnValueOnce({
52
+ ...context.atlantisContextDefaultValues,
53
+ locale: "fr",
54
+ });
42
55
  const { result } = renderHook(useAtlantisI18n);
43
56
 
44
57
  expect(result.current.t("cancel")).toBe("Cancel");
@@ -50,4 +63,92 @@ describe("useAtlantisI18n", () => {
50
63
  expect(Object.keys(en)).toEqual(Object.keys(es));
51
64
  });
52
65
  });
66
+
67
+ describe("formatDate", () => {
68
+ it("should return the formatted date", () => {
69
+ const { result } = renderHook(useAtlantisI18n);
70
+ expect(result.current.formatDate(testDate)).toBe("Jan 1, 2020");
71
+ });
72
+
73
+ it("should return the date formatted for es", () => {
74
+ spy.mockReturnValueOnce({
75
+ ...context.atlantisContextDefaultValues,
76
+ locale: "es",
77
+ });
78
+
79
+ const { result } = renderHook(useAtlantisI18n);
80
+ expect(result.current.formatDate(testDate)).toBe("1 ene 2020");
81
+ });
82
+
83
+ describe("Timezone", () => {
84
+ it.each([
85
+ ["America/New_York", "Dec 31, 2019"],
86
+ ["America/Chicago", "Dec 31, 2019"],
87
+ ["America/Denver", "Dec 31, 2019"],
88
+ ["Europe/London", "Jan 1, 2020"],
89
+ ["Australia/Sydney", "Jan 1, 2020"],
90
+ ])("should return the %s time", (timeZone, expected) => {
91
+ spy.mockReturnValueOnce({
92
+ ...context.atlantisContextDefaultValues,
93
+ timeZone,
94
+ });
95
+
96
+ const { result } = renderHook(useAtlantisI18n);
97
+ expect(result.current.formatDate(testDate)).toBe(expected);
98
+ });
99
+ });
100
+ });
101
+
102
+ describe("formatTime", () => {
103
+ it("should return the formatted time", () => {
104
+ const { result } = renderHook(useAtlantisI18n);
105
+ expect(result.current.formatTime(testDate)).toBe("12:00 AM");
106
+ });
107
+
108
+ it("should return the date formatted for es", () => {
109
+ spy.mockReturnValueOnce({
110
+ ...context.atlantisContextDefaultValues,
111
+ locale: "es",
112
+ });
113
+
114
+ const { result } = renderHook(useAtlantisI18n);
115
+ expect(result.current.formatTime(testDate)).toBe("00:00");
116
+ });
117
+
118
+ describe("Timezone", () => {
119
+ it.each([
120
+ ["America/New_York", "7:00 PM"],
121
+ ["America/Chicago", "6:00 PM"],
122
+ ["America/Denver", "5:00 PM"],
123
+ ["Europe/London", "12:00 AM"],
124
+ ["Australia/Sydney", "11:00 AM"],
125
+ ])("should return the %s zoned time", (timeZone, expected) => {
126
+ spy.mockReturnValueOnce({
127
+ ...context.atlantisContextDefaultValues,
128
+ timeZone,
129
+ });
130
+
131
+ const { result } = renderHook(useAtlantisI18n);
132
+ expect(result.current.formatTime(testDate)).toBe(expected);
133
+ });
134
+
135
+ it.each([
136
+ ["America/New_York", "8:00 PM"],
137
+ ["America/Chicago", "7:00 PM"],
138
+ ["America/Denver", "6:00 PM"],
139
+ ["Europe/London", "1:00 AM"],
140
+ ["Australia/Sydney", "10:00 AM"],
141
+ ])("should return the %s spring zoned time", (timeZone, expected) => {
142
+ spy.mockReturnValueOnce({
143
+ ...context.atlantisContextDefaultValues,
144
+ timeZone,
145
+ });
146
+
147
+ const { result } = renderHook(useAtlantisI18n);
148
+ expect(result.current.formatTime(dateAfterSpringForward)).toBe(
149
+ expected,
150
+ );
151
+ });
152
+ });
153
+ });
53
154
  });
@@ -1,20 +1,56 @@
1
+ import { useCallback } from "react";
1
2
  import en from "./locales/en.json";
2
3
  import es from "./locales/es.json";
4
+ import { dateFormatter } from "./utils/dateFormatter";
3
5
  import { useAtlantisContext } from "../../AtlantisContext";
4
6
 
5
7
  export type I18nKeys = keyof typeof en;
6
8
 
7
9
  export interface useAtlantisI18nValue {
10
+ /**
11
+ * The set locale based on the AtlantisContext.
12
+ */
8
13
  readonly locale: string;
14
+
15
+ /**
16
+ * Returns the translated string depending on the locale. This accepts a 2nd
17
+ * param for string interpolation.
18
+ */
9
19
  readonly t: (message: I18nKeys, values?: Record<string, string>) => string;
20
+
21
+ /**
22
+ * Returns a formatted date string based on the locale and the `dateFormat`
23
+ * set in AtlantisContext.
24
+ */
25
+ readonly formatDate: (date: Date) => string;
26
+
27
+ /**
28
+ * Returns a formatted time string based on the locale and the `timeFormat`
29
+ * set in AtlantisContext.
30
+ */
31
+ readonly formatTime: (date: Date) => string;
10
32
  }
11
33
 
12
34
  export function useAtlantisI18n(): useAtlantisI18nValue {
13
- const { locale } = useAtlantisContext();
14
- const t = (messageKey: keyof typeof en, values?: Record<string, string>) =>
15
- formatMessage(messageKey, values, locale);
35
+ const { locale, dateFormat, timeFormat, timeZone } = useAtlantisContext();
36
+
37
+ const t = useCallback(
38
+ (messageKey: keyof typeof en, values?: Record<string, string>) =>
39
+ formatMessage(messageKey, values, locale),
40
+ [formatMessage, locale],
41
+ );
42
+
43
+ const formatDate = useCallback(
44
+ (date: Date) => dateFormatter(date, dateFormat, { locale, timeZone }),
45
+ [dateFormatter, locale],
46
+ );
47
+
48
+ const formatTime = useCallback(
49
+ (date: Date) => dateFormatter(date, timeFormat, { locale, timeZone }),
50
+ [dateFormatter, locale],
51
+ );
16
52
 
17
- return { locale, t };
53
+ return { locale, t, formatDate, formatTime };
18
54
  }
19
55
 
20
56
  function getLocalizedStrings(locale: string): typeof en {
@@ -0,0 +1,31 @@
1
+ import { format } from "date-fns";
2
+ import { utcToZonedTime } from "date-fns-tz";
3
+ import { es } from "date-fns/locale";
4
+
5
+ interface DateFormatterOptions {
6
+ readonly locale: string;
7
+ readonly timeZone: string;
8
+ }
9
+
10
+ export function dateFormatter(
11
+ date: Date,
12
+ dateTimeFormat: string,
13
+ { locale, timeZone }: DateFormatterOptions,
14
+ ): string {
15
+ const zonedTime = utcToZonedTime(date, timeZone);
16
+ return format(zonedTime, dateTimeFormat, {
17
+ locale: getDateFnsLocale(locale),
18
+ });
19
+ }
20
+
21
+ /**
22
+ * Change locale string to date-fns locale object.
23
+ */
24
+ function getDateFnsLocale(locale?: string): Locale | undefined {
25
+ switch (locale) {
26
+ case "es":
27
+ return es;
28
+ default:
29
+ return;
30
+ }
31
+ }