diff --git a/CHANGELOG.md b/CHANGELOG.md index ba80e3a..a52f234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Add + +- Localization, #70. + ## [0.0.8] - 2022-05-12 No package update. diff --git a/package.json b/package.json index c7a7fc9..193e78e 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,14 @@ "prepare": "husky install" }, "type": "module", - "dependencies": {}, + "dependencies": { + "@date-io/date-fns": "^2.13.2", + "@date-io/date-fns-jalali": "^2.13.2", + "@date-io/dayjs": "^2.13.2", + "@date-io/jalaali": "^2.13.2", + "@date-io/luxon": "^2.13.2", + "@date-io/moment": "^2.13.2" + }, "devDependencies": { "@chakra-ui/icons": "^1.1.7", "@chakra-ui/react": "^1.8.6", @@ -53,6 +60,7 @@ "@typescript-eslint/eslint-plugin": "^5.19.0", "@typescript-eslint/parser": "^5.19.0", "@vitejs/plugin-react": "^1.0.7", + "date-fns": "^2.28.0", "eslint": "^8.13.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-react": "^7.29.4", diff --git a/src/LocalizationProvider/LocalizationProvider.tsx b/src/LocalizationProvider/LocalizationProvider.tsx new file mode 100644 index 0000000..0cd461a --- /dev/null +++ b/src/LocalizationProvider/LocalizationProvider.tsx @@ -0,0 +1,62 @@ +// Ref: https://github.com/mui/mui-x/blob/5dc5bd4b04eecf21c45cf6ea1169727ba5ab3be3/packages/x-date-pickers/src/LocalizationProvider/LocalizationProvider.tsx + +import * as React from "react"; +import { DateIOFormats } from "@date-io/core/IUtils"; +import { IUtils } from "@date-io/core/IUtils"; + +type DateAdapter = IUtils; + +export interface DateAdapterContextValue { + utils: DateAdapter; +} + +export const DateAdapterContext = + React.createContext | null>(null); +if (process.env.NODE_ENV !== "production") { + DateAdapterContext.displayName = "DateAdapterContext"; +} + +export interface LocalizationProviderProps { + children?: React.ReactNode; + /** DateIO adapter class function */ + dateAdapter: new (...args: any) => DateAdapter; + /** Formats that are used for the child dropdown */ + dateFormats?: Partial; + /** + * Date library instance you are using, if it has some global overrides + * ```jsx + * dateLibInstance={momentTimeZone} + * ``` + */ + dateLibInstance?: any; + /** Locale for the date library you are using */ + locale?: string | object; +} + +/** + * @ignore - do not document. + */ +export function LocalizationProvider(props: LocalizationProviderProps) { + const { + children, + dateAdapter: Utils, + dateFormats, + dateLibInstance, + locale, + } = props; + const utils = React.useMemo( + () => + new Utils({ locale, formats: dateFormats, instance: dateLibInstance }), + [Utils, locale, dateFormats, dateLibInstance] + ); + + const contextValue: DateAdapterContextValue = React.useMemo(() => { + return { utils }; + }, [utils]); + + return ( + + {children} + + ); +} diff --git a/src/LocalizationProvider/index.ts b/src/LocalizationProvider/index.ts new file mode 100644 index 0000000..552f8c1 --- /dev/null +++ b/src/LocalizationProvider/index.ts @@ -0,0 +1 @@ +export * from "./LocalizationProvider"; diff --git a/src/LocalizationProvider/useUtils.ts b/src/LocalizationProvider/useUtils.ts new file mode 100644 index 0000000..4e9a652 --- /dev/null +++ b/src/LocalizationProvider/useUtils.ts @@ -0,0 +1,12 @@ +import * as React from "react"; +import { DateAdapterContext } from "./LocalizationProvider"; + +export const useUtils = () => { + const localization = React.useContext(DateAdapterContext); + + if (localization == null) { + return null; + } + + return localization.utils; +}; diff --git a/src/adapters/date-fns.ts b/src/adapters/date-fns.ts new file mode 100644 index 0000000..923e06b --- /dev/null +++ b/src/adapters/date-fns.ts @@ -0,0 +1 @@ +export { default } from "@date-io/date-fns"; diff --git a/src/adapters/dayjs.ts b/src/adapters/dayjs.ts new file mode 100644 index 0000000..4adf95e --- /dev/null +++ b/src/adapters/dayjs.ts @@ -0,0 +1 @@ +export { default } from "@date-io/dayjs"; diff --git a/src/adapters/luxon.ts b/src/adapters/luxon.ts new file mode 100644 index 0000000..7f5718c --- /dev/null +++ b/src/adapters/luxon.ts @@ -0,0 +1 @@ +export { default } from "@date-io/luxon"; diff --git a/src/adapters/moment.ts b/src/adapters/moment.ts new file mode 100644 index 0000000..d0728c1 --- /dev/null +++ b/src/adapters/moment.ts @@ -0,0 +1 @@ +export { default } from "@date-io/moment"; diff --git a/src/index.ts b/src/index.ts index 60f4507..d467ddf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,3 +7,4 @@ export { export * from "./use-date-select"; export { getDateString } from "./date-string"; export * from "./types"; +export * from "./LocalizationProvider"; diff --git a/src/use-date-select.ts b/src/use-date-select.ts index c754547..c21cf30 100644 --- a/src/use-date-select.ts +++ b/src/use-date-select.ts @@ -1,7 +1,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { range } from "./range"; import { compileDateString, parseDateString } from "./date-string"; -import { Option, Options } from "./types"; +import { Options } from "./types"; +import { useUtils } from "./LocalizationProvider/useUtils"; const DEFAULT_MIN_YEAR = 1960; const DEFAULT_MAX_YEAR = new Date().getFullYear(); @@ -13,17 +14,6 @@ function convertToSelectValue(value: number): string { return value.toString(); } -function compileOption(value: string): Option { - return { value, label: value }; // TODO: Be customizable for localization -} - -const monthOptions: Options = range(1, 12).map((i) => - compileOption(convertToSelectValue(i)) -); -const dayOptions: Options = range(1, 31).map((i) => - compileOption(convertToSelectValue(i)) -); - interface DefaultDateOptions { defaultYear?: number | "now"; defaultMonth?: number | "now"; @@ -169,18 +159,61 @@ export const useDateSelect = ( } }, [setDate, value]); - const yearOptions = useMemo(() => { + // Generate year, month, and day arrays using the locale. + const utils = useUtils(); + + const rawYearOptions = useMemo(() => { const minYear = opts.minYear != null ? opts.minYear : DEFAULT_MIN_YEAR; const maxYear = opts.maxYear != null ? opts.maxYear : DEFAULT_MAX_YEAR; - const raw = range(minYear, maxYear).map((i) => { - const s = convertToSelectValue(i); - return { value: s, label: s }; + const _yearOptions = range(minYear, maxYear).map((i) => { + const label = utils + ? utils.format(new Date(i, 0, 1), "year") + : i.toString(); + return { value: convertToSelectValue(i), label }; }); - if (!raw.some((o) => o.value === state.yearValue)) { - return raw.concat(compileOption(state.yearValue)); + return _yearOptions; + }, [opts.minYear, opts.maxYear, utils]); + + // If the value of `state.yearValue` is not included in the year select options, add it. + const yearOptions = useMemo(() => { + if ( + state.yearValue !== "" && + !rawYearOptions.some((o) => o.value === state.yearValue) + ) { + let label: string; + try { + label = utils + ? utils.format( + new Date(parseSelectValue(state.yearValue), 0, 1), + "year" + ) + : state.yearValue; + } catch { + label = state.yearValue; + } + return rawYearOptions.concat({ label, value: state.yearValue }); } - return raw; - }, [opts.minYear, opts.maxYear, state.yearValue]); + + return rawYearOptions; + }, [rawYearOptions, state.yearValue]); + + const [monthOptions, dayOptions] = useMemo(() => { + const _monthOptions = range(1, 12).map((i) => { + const label = utils + ? utils.format(new Date(1960, i - 1, 1), "monthShort") + : i.toString(); + return { value: convertToSelectValue(i), label }; + }); + + const _dayOptions: Options = range(1, 31).map((i) => { + const label = utils + ? utils.format(new Date(1960, 1, i), "dayOfMonth") + : i.toString(); + return { value: convertToSelectValue(i), label }; + }); + + return [_monthOptions, _dayOptions]; + }, [utils]); return { yearValue: state.yearValue, diff --git a/website/Main.mdx b/website/Main.mdx index 57ec38d..914fb67 100644 --- a/website/Main.mdx +++ b/website/Main.mdx @@ -74,6 +74,21 @@ import PartialDefaultValueNowSampleCode from "./samples/options/partial-default- You can set `"now"` to `defaultYear`, `defaultMonth`, and `defaultDay`. +### Localization + +import LocalizationSampleCode from "./samples/options/localization?raw"; + + + +You can set the locale through `` wrapping the component. + +It accesses the date library you are using to use locale functionality through the "adapter" object which is, in the example above, `react-ymd-date-select/adapters/date-fns` for [`date-fns`](https://date-fns.org/). +Please select an appropriate adapter for your project. + +With this design, this library can reuse the already installed date functions and can save the final bundle size. + +Note that the date library such as `date-fns` is NOT installed along with `react-ymd-date-select`, so you have to install it separately. + ### Hide day import HideDaySampleCode from "./samples/options/hide-day?raw"; diff --git a/website/components/CodePreview/scope.ts b/website/components/CodePreview/scope.ts index 835960d..186c024 100644 --- a/website/components/CodePreview/scope.ts +++ b/website/components/CodePreview/scope.ts @@ -5,6 +5,12 @@ import * as reactYmdDateSelect from "react-ymd-date-select"; import * as vanillaPreset from "react-ymd-date-select/presets/vanilla"; import * as chakraPreset from "react-ymd-date-select/presets/chakra-ui"; import * as muiPreset from "react-ymd-date-select/presets/mui"; +import { default as DateFnsAdapter } from "react-ymd-date-select/adapters/date-fns"; +import dateFnsFrLocale from "date-fns/locale/fr"; +import dateFnsRuLocale from "date-fns/locale/ru"; +import dateFnsDeLocale from "date-fns/locale/de"; +import dateFnsEnLocale from "date-fns/locale/en-US"; +import dateFnsJaLocale from "date-fns/locale/ja"; export const scope = { import: { @@ -15,5 +21,11 @@ export const scope = { "react-ymd-date-select/presets/vanilla": vanillaPreset, "react-ymd-date-select/presets/chakra-ui": chakraPreset, "react-ymd-date-select/presets/material": muiPreset, + "react-ymd-date-select/adapters/date-fns": DateFnsAdapter, + "date-fns/locale/fr": dateFnsFrLocale, + "date-fns/locale/ru": dateFnsRuLocale, + "date-fns/locale/de": dateFnsDeLocale, + "date-fns/locale/en-US": dateFnsEnLocale, + "date-fns/locale/ja": dateFnsJaLocale, }, }; diff --git a/website/components/EyeCatchDateSelect/index.tsx b/website/components/EyeCatchDateSelect/index.tsx index 7fbe505..518b728 100644 --- a/website/components/EyeCatchDateSelect/index.tsx +++ b/website/components/EyeCatchDateSelect/index.tsx @@ -1,7 +1,12 @@ import { useState } from "react"; import { FormControl, FormErrorMessage, Select } from "@chakra-ui/react"; import styled from "@emotion/styled"; -import { useDateSelect, getDateString } from "react-ymd-date-select"; +import DateFnsAdapter from "@date-io/date-fns"; +import { + useDateSelect, + getDateString, + LocalizationProvider, +} from "react-ymd-date-select"; const dropdownIconColor = "#be5f6f"; const errorColor = "#fcdfff"; @@ -75,4 +80,12 @@ function EyeCatchDateSelect() { ); } -export default EyeCatchDateSelect; +function LocalizedEyeCatchDateSelect() { + return ( + + + + ); +} + +export default LocalizedEyeCatchDateSelect; diff --git a/website/samples/options/localization.tsx b/website/samples/options/localization.tsx new file mode 100644 index 0000000..c7e3cb7 --- /dev/null +++ b/website/samples/options/localization.tsx @@ -0,0 +1,58 @@ +import { useState } from "react"; +import frLocale from "date-fns/locale/fr"; +import ruLocale from "date-fns/locale/ru"; +import deLocale from "date-fns/locale/de"; +import enLocale from "date-fns/locale/en-US"; +import jaLocale from "date-fns/locale/ja"; +import { LocalizationProvider } from "react-ymd-date-select"; +import { DateSelect } from "react-ymd-date-select/presets/vanilla"; +/* Select the appropriate adapter for the library you are using as below. */ +import DateAdapter from "react-ymd-date-select/adapters/date-fns"; +// import DateAdapter from "react-ymd-date-select/adapters/dayjs"; +// import DateAdapter from "react-ymd-date-select/adapters/luxon"; +// import DateAdapter from "react-ymd-date-select/adapters/moment"; + +const localeMap = { + en: enLocale, + fr: frLocale, + ru: ruLocale, + de: deLocale, + ja: jaLocale, +}; + +type Locale = keyof typeof localeMap; + +function Sample() { + const [locale, setLocale] = useState("en"); + const [date, setDate] = useState(""); + + return ( +
+
+ +
+ + + +

Selected date is: {date}

+
+
+ ); +} + +export default Sample; diff --git a/yarn.lock b/yarn.lock index 2be5efe..7ac7c84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -818,6 +818,53 @@ resolved "https://registry.yarnpkg.com/@cush/relative/-/relative-1.0.0.tgz#8cd1769bf9bde3bb27dac356b1bc94af40f6cc16" integrity sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA== +"@date-io/core@^2.13.2": + version "2.13.2" + resolved "https://registry.yarnpkg.com/@date-io/core/-/core-2.13.2.tgz#63983fbe2816a758d3fe51857e88d9170c40073c" + integrity sha512-lAUDhC5kpzlxa00BxfqENBgerbGI5ojuKQpXLGZCTrqT1rQR+vrp2rwf0I+H2KlM2z3N1ldyQuANmzZ+ehomog== + +"@date-io/date-fns-jalali@^2.13.2": + version "2.13.2" + resolved "https://registry.yarnpkg.com/@date-io/date-fns-jalali/-/date-fns-jalali-2.13.2.tgz#b166dd63cb8cb8f15289b90e06831f86338e8ceb" + integrity sha512-395rRc28wkmLiiMuwGOt31paQpHPXAEXbfXLTVsNL/eWi16+ndX9Jgu6RKsT9apbeK7nCT/1Xz/9LkvgFxyISw== + dependencies: + "@date-io/core" "^2.13.2" + +"@date-io/date-fns@^2.13.2": + version "2.13.2" + resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-2.13.2.tgz#d0abddc7e76dbaeff92b876b7ad839729cc682cf" + integrity sha512-Pq0xBH6cvEg5IsOs7Olkk1PHFFJFTM34OT5mk/9ND1ied4RGhLNeLYRwbyCThZ29jolqPsV2HBs9wo2QTiNIkg== + dependencies: + "@date-io/core" "^2.13.2" + +"@date-io/dayjs@^2.13.2": + version "2.13.2" + resolved "https://registry.yarnpkg.com/@date-io/dayjs/-/dayjs-2.13.2.tgz#b337e92bd9d3fe896619f82d68f23209e2b03f8f" + integrity sha512-uP/Bvr+QfzpLN3JGX/x3uJtGnnSWckn7e500Wn+pVNVvIleaXSJxDbE5IAz1P7WwQrnSuxuBI1e6Sl8J/njzKQ== + dependencies: + "@date-io/core" "^2.13.2" + +"@date-io/jalaali@^2.13.2": + version "2.13.2" + resolved "https://registry.yarnpkg.com/@date-io/jalaali/-/jalaali-2.13.2.tgz#42561a3dc32e11e6565a261fea2e4bfd5f986ff9" + integrity sha512-/diAXI3PEu4LqHiwkCY267A5lWxMH46/Q70pKuLOQx586Ix0kUNHXbCzSo5BuTsw5JdFjEwAWFS8Ik2THmrGbw== + dependencies: + "@date-io/moment" "^2.13.2" + +"@date-io/luxon@^2.13.2": + version "2.13.2" + resolved "https://registry.yarnpkg.com/@date-io/luxon/-/luxon-2.13.2.tgz#57476ff987488e12f560f5d5b2831898104ca094" + integrity sha512-/LeUXsBeM9DITZliaAzKYBu6GtaD3AT5nQR5u7AasjUz1JlYPCTjV5z8b+wnY/jm4bFz5AhgK+TMHGaZoMJW+w== + dependencies: + "@date-io/core" "^2.13.2" + +"@date-io/moment@^2.13.2": + version "2.13.2" + resolved "https://registry.yarnpkg.com/@date-io/moment/-/moment-2.13.2.tgz#2702aa27571742436efd6dd3f0b8f80745bcf1a4" + integrity sha512-6uWeCS44srr86MaeUS0mS10T1g7WKfqZ47r1rEc+8xcS3640TIPNf/pg30kwprZKAM7gtbZu6vW6pe1MEPbcKA== + dependencies: + "@date-io/core" "^2.13.2" + "@emotion/babel-plugin@^11.7.1": version "11.9.2" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.9.2.tgz#723b6d394c89fb2ef782229d92ba95a740576e95" @@ -1937,6 +1984,11 @@ csstype@3.0.10, csstype@3.0.9, csstype@^3.0.10, csstype@^3.0.11, csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA== +date-fns@^2.28.0: + version "2.28.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2" + integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw== + debug@^4.0.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"