Skip to content

Melreal/dates on fns #619

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

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion assets/flash-notifications-bundle.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 25 additions & 25 deletions assets/new-request-form-bundle.js

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions assets/shared-bundle.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"eslint": "eslint src",
"prepare": "husky install",
"download-locales": "node ./bin/update-translations",
"test": "jest",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"test-a11y": "node bin/lighthouse/index.js",
"i18n:extract": "node bin/extract-strings.mjs",
"i18n:update-translations": "node bin/update-modules-translations.mjs",
Expand All @@ -32,6 +32,7 @@
"@zendeskgarden/react-tooltips": "8.76.9",
"@zendeskgarden/react-typography": "8.76.9",
"@zendeskgarden/svg-icons": "^6.34.0",
"date-fns": "^4.1.0",
"dompurify": "3.2.4",
"eslint-plugin-check-file": "^2.6.2",
"i18next": "^23.10.1",
Expand Down
47 changes: 38 additions & 9 deletions src/modules/new-request-form/NewRequestForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { AnswerBot, Field, RequestForm } from "./data-types";
import { Fragment, useCallback, useState } from "react";
import { Fragment, useCallback, useState, useEffect } from "react";
import { Input } from "./fields/Input";
import { TextArea } from "./fields/textarea/TextArea";
import { DropDown } from "./fields/DropDown";
Expand All @@ -24,6 +24,7 @@
import { Paragraph } from "@zendeskgarden/react-typography";
import { LookupField } from "./fields/LookupField";
import type { Organization } from "./data-types/Organization";
import { getLocale, parseAndValidateDate } from "./datePickerLanguageParser";

export interface NewRequestFormProps {
requestForm: RequestForm;
Expand Down Expand Up @@ -121,6 +122,13 @@
? organizations[0]?.id?.toString()
: null;

const [localeObject, setLocaleObject] = useState(null);
const [dateObject, setdateObject] = useState(null);

Check failure on line 126 in src/modules/new-request-form/NewRequestForm.tsx

View workflow job for this annotation

GitHub Actions / Lint JS files

'dateObject' is assigned a value but never used

Check failure on line 126 in src/modules/new-request-form/NewRequestForm.tsx

View workflow job for this annotation

GitHub Actions / Lint JS files

'setdateObject' is assigned a value but never used

useEffect(() => {
getLocale(baseLocale).then((locale) => setLocaleObject(locale));
}, [baseLocale]);

const handleChange = useCallback(
(field: Field, value: Field["value"]) => {
setTicketFields(
Expand Down Expand Up @@ -276,16 +284,26 @@
field={field}
onChange={(value) => handleChange(field, value)}
/>
{field.value === "task" && (
{field.value === "task" && localeObject ? (
<DatePicker
field={dueDateField}
key={field.name}
field={field}
locale={baseLocale}
valueFormat="dateTime"
onChange={(value) => {
handleDueDateChange(value);
}}
valueFormat="date"
onChange={(value) => handleDueDateChange(value)}
customParseDate={(inputString) =>
parseAndValidateDate(inputString, localeObject)
}
/>
)}
) : field.value === "task" ? (
<DatePicker
key={field.name}
field={field}
locale={baseLocale}
valueFormat="date"
onChange={(value) => handleDueDateChange(value)}
/>
) : null}
</Fragment>
);
case "checkbox":
Expand All @@ -297,7 +315,18 @@
/>
);
case "date":
return (
return localeObject ? (
<DatePicker
key={field.name}
field={field}
locale={baseLocale}
valueFormat="date"
onChange={(value) => handleChange(field, value)}
customParseDate={(inputString) =>
parseAndValidateDate(inputString, localeObject)
}
/>
) : (
<DatePicker
key={field.name}
field={field}
Expand Down
124 changes: 124 additions & 0 deletions src/modules/new-request-form/datePickerLanguageParser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// maps our help center official parent/baseLocales to date-fns locales for the purpose
// of allowing manually entered dates to be entered into the datepicker and validated in different locales
// by checking their date fns matches the date-fns format expectation

// available date-fns locales: https://github.com/date-fns/date-fns/tree/main/src/locale

// Official zendesk languages so far not supported by date-fns (format fallback to en-us):
// fil (Filipino)
// persian - have used Iran Persian farsi (fa-IR) instead which is available
// no - instead of norwegian official we use the available Norwegian Bokmal
// en-gb is not one of our baseLocales - so I've used en-150 which is the closest match for "European English"

import { isValid, isBefore, parse, parseISO } from "date-fns";


Check failure on line 15 in src/modules/new-request-form/datePickerLanguageParser.js

View workflow job for this annotation

GitHub Actions / Lint JS files

Delete `⏎`
export const supportedLanguages = {
ar: () => import("date-fns/locale/ar"),
bg: () => import("date-fns/locale/bg"),
cs: () => import("date-fns/locale/cs"),
da: () => import("date-fns/locale/da"),
de: () => import("date-fns/locale/de"),
el: () => import("date-fns/locale/el"),
"en-gb": () => import("date-fns/locale/en-GB"),
"en-us": () => import("date-fns/locale/en-US"),
es: () => import("date-fns/locale/es"),
fa: () => import("date-fns/locale/fa-IR"),
fi: () => import("date-fns/locale/fi"),
fr: () => import("date-fns/locale/fr"),
he: () => import("date-fns/locale/he"),
hi: () => import("date-fns/locale/hi"),
hu: () => import("date-fns/locale/hu"),
id: () => import("date-fns/locale/id"),
it: () => import("date-fns/locale/it"),
ja: () => import("date-fns/locale/ja"),
ko: () => import("date-fns/locale/ko"),
ms: () => import("date-fns/locale/ms"),
nl: () => import("date-fns/locale/nl"),
nn: () => import("date-fns/locale/nn"),
no: () => import("date-fns/locale/nb"),
pl: () => import("date-fns/locale/pl"),
pt: () => import("date-fns/locale/pt"),
ro: () => import("date-fns/locale/ro"),
ru: () => import("date-fns/locale/ru"),
sv: () => import("date-fns/locale/sv"),
th: () => import("date-fns/locale/th"),
tr: () => import("date-fns/locale/tr"),
vi: () => import("date-fns/locale/vi"),
"zh-cn": () => import("date-fns/locale/zh-CN"),
"zh-tw": () => import("date-fns/locale/zh-TW")

Check failure on line 49 in src/modules/new-request-form/datePickerLanguageParser.js

View workflow job for this annotation

GitHub Actions / Lint JS files

Insert `,`
};

const fallbackLanguages = {
"en-001": "en-gb",
"en-150": "en-gb",
"en-au": "en-gb",
"en-my": "en-gb",
"en-ph": "en-gb",
"en-se": "en-gb",
"es-419": "es",
"es-es": "es",
"it-ch": "it",
"fr-ca": "fr",
"nl-be": "nl",
"pt-br": "pt"

Check failure on line 64 in src/modules/new-request-form/datePickerLanguageParser.js

View workflow job for this annotation

GitHub Actions / Lint JS files

Insert `,`
};

Check failure on line 65 in src/modules/new-request-form/datePickerLanguageParser.js

View workflow job for this annotation

GitHub Actions / Lint JS files

Delete `⏎`


const defaultLocale = "en-us";

export const localeToLoad = (locale) => {
locale = fallbackLanguages[locale] || locale;
const availableLocales = Object.keys(supportedLanguages);

if (availableLocales.includes(locale)) {
return locale;
}

return defaultLocale;
};

export const parseAndValidateDate = (value, locale) => {
const MINIMUM_DATE = new Date(1001, 0, 0);

const validDateFormat = (date) =>
isValid(date) && !isBefore(date, MINIMUM_DATE);

const parsedShortFormat = parse(value, "P", new Date(), {
locale: locale,
});

if (validDateFormat(parsedShortFormat)) {
return parsedShortFormat;
}

const parsedISOvalue = parseISO(value);
if (validDateFormat(parsedISOvalue)) {
return parsedISOvalue;
}

const parsedMidFormat = parse(value, "PP", new Date(), {
locale: locale,
});

if (validDateFormat(parsedMidFormat)) {
return parsedMidFormat;
}

const parsedLongFormat = parse(value, "PPP", new Date(), {
locale: locale,
});

if (validDateFormat(parsedLongFormat)) {
return parsedLongFormat;
}

return new Date(NaN);
};

export async function getLocale(localeCode) {
const validLocale = localeToLoad(localeCode);
const locale = await supportedLanguages[validLocale]();

return locale[localeCode] || locale.default;
}
79 changes: 79 additions & 0 deletions src/modules/new-request-form/datePickerLanguageParser.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { parse, parseISO } from "date-fns";

Check failure on line 1 in src/modules/new-request-form/datePickerLanguageParser.spec.js

View workflow job for this annotation

GitHub Actions / Lint JS files

'parse' is defined but never used

Check failure on line 1 in src/modules/new-request-form/datePickerLanguageParser.spec.js

View workflow job for this annotation

GitHub Actions / Lint JS files

'parseISO' is defined but never used
import {
supportedLanguages,
localeToLoad,
getLocale,
parseAndValidateDate,
} from "./datePickerLanguageParser";

describe("datePickerLanguageParser", () => {

Check failure on line 9 in src/modules/new-request-form/datePickerLanguageParser.spec.js

View workflow job for this annotation

GitHub Actions / Lint JS files

'describe' is not defined
describe("supportedLanguages", () => {

Check failure on line 10 in src/modules/new-request-form/datePickerLanguageParser.spec.js

View workflow job for this annotation

GitHub Actions / Lint JS files

'describe' is not defined
it("imports all items successfully", async () => {
for (var key in supportedLanguages) {
const locale = await getLocale(key);

expect(locale).not.toBe(null);
expect(locale).toBeDefined();
expect(locale.formatLong).toBeDefined();
}
});
});

describe("localeToLoad", () => {
it("returns a close match when specific locales are not available", () => {
expect(localeToLoad("en-001")).toEqual("en-gb");
expect(localeToLoad("en-150")).toEqual("en-gb");
expect(localeToLoad("en-au")).toEqual("en-gb");
expect(localeToLoad("en-my")).toEqual("en-gb");
expect(localeToLoad("en-ph")).toEqual("en-gb");
expect(localeToLoad("en-se")).toEqual("en-gb");
expect(localeToLoad("es-419")).toEqual("es");
expect(localeToLoad("es-es")).toEqual("es");
expect(localeToLoad("it-ch")).toEqual("it");
expect(localeToLoad("fr-ca")).toEqual("fr");
expect(localeToLoad("nl-be")).toEqual("nl");
expect(localeToLoad("pt-br")).toEqual("pt");
});

it("returns the default locale when given an unavailable locale", () => {
expect(localeToLoad("fil")).toEqual("en-us");
});
});

describe("parseAndValidateDate", () => {
it("considers long format dates in french valid", async () => {
const locale = await getLocale("fr");
const parseResult = parseAndValidateDate("6 avril 2026", locale);

expect(parseResult.toString()).toContain("Apr 06 2026");
});

it("considers long format dates in en-gb valid", async () => {
const locale = await getLocale("en-gb");
const parseResult = parseAndValidateDate("6 march 2026", locale);

expect(parseResult.toString()).toContain("Mar 06 2026");
});

it("considers long format dates in short format en valid", async () => {
const locale = await getLocale("en-001");
const parseResult = parseAndValidateDate("6/03/2026", locale);

expect(parseResult.toString()).toContain("Mar 06 2026");
});

it("considers long format dates in short format en au valid", async () => {
const locale = await getLocale("en-au");
const parseResult = parseAndValidateDate("2026-06-03", locale);

expect(parseResult.toString()).toContain("Jun 03 2026");
});

it("considers long format dates in japanese valid", async () => {
const locale = await getLocale("ja");
const parseResult = parseAndValidateDate("2025年3月15日", locale);

expect(parseResult.toString()).toContain("Mar 15 2025");
});
});
});
1 change: 1 addition & 0 deletions src/modules/new-request-form/fields/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
field: Field;
locale: string;
valueFormat: "date" | "dateTime";
customParseDate?: (inputValue: string) => Date;
onChange: (value: string) => void;
}

Expand Down Expand Up @@ -84,7 +85,7 @@
<GardenField>
<Label>
{label}
{required && <Span aria-hidden="true">*</Span>}

Check warning on line 88 in src/modules/new-request-form/fields/DatePicker.tsx

View workflow job for this annotation

GitHub Actions / Lint JS files

Do not use hardcoded content as the children of the Span component
</Label>
{description && (
<Hint dangerouslySetInnerHTML={{ __html: description }} />
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"allowJs": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"noImplicitOverride": true,
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5759,6 +5759,11 @@ date-fns@^3.6.0:
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf"
integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==

date-fns@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-4.1.0.tgz#64b3d83fff5aa80438f5b1a633c2e83b8a1c2d14"
integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==

dateformat@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
Expand Down
Loading