Skip to content

Commit

Permalink
feat: add new numerals prop (#2647)
Browse files Browse the repository at this point in the history
* feat: add new `numerals` prop

* Update docs

* Update playground
  • Loading branch information
gpbl authored Dec 26, 2024
1 parent 8a67562 commit d09e2ac
Show file tree
Hide file tree
Showing 11 changed files with 192 additions and 100 deletions.
21 changes: 0 additions & 21 deletions examples/NumberingSystem.test.tsx

This file was deleted.

33 changes: 0 additions & 33 deletions examples/NumberingSystem.tsx

This file was deleted.

18 changes: 18 additions & 0 deletions examples/Numerals.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from "react";

import { render } from "@testing-library/react";

import { grid } from "@/test/elements";

import { Numerals } from "./Numerals";

const today = new Date(2025, 10, 25);

beforeAll(() => jest.setSystemTime(today));
afterAll(() => jest.useRealTimers());

test("should use Devanagari numerals", () => {
render(<Numerals />);

expect(grid()).toHaveTextContent("२५");
});
8 changes: 8 additions & 0 deletions examples/Numerals.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from "react";

import { hi } from "date-fns/locale/hi";
import { DayPicker } from "react-day-picker";

export function Numerals() {
return <DayPicker numerals="deva" locale={hi} />;
}
2 changes: 1 addition & 1 deletion examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export * from "./MultipleMinMax";
export * from "./MultipleRequired";
export * from "./MultipleMonths";
export * from "./MultipleMonthsPaged";
export * from "./NumberingSystem";
export * from "./Numerals";
export * from "./OutsideDays";
export * from "./PastDatesDisabled";
export * from "./Persian";
Expand Down
4 changes: 3 additions & 1 deletion src/DayPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ export function DayPicker(props: DayPickerProps) {
firstWeekContainsDate: props.firstWeekContainsDate,
useAdditionalWeekYearTokens: props.useAdditionalWeekYearTokens,
useAdditionalDayOfYearTokens: props.useAdditionalDayOfYearTokens,
timeZone: props.timeZone
timeZone: props.timeZone,
numerals: props.numerals
},
props.dateLib
);
Expand All @@ -69,6 +70,7 @@ export function DayPicker(props: DayPickerProps) {
props.useAdditionalWeekYearTokens,
props.useAdditionalDayOfYearTokens,
props.timeZone,
props.numerals,
props.dateLib,
props.components,
props.formatters,
Expand Down
62 changes: 60 additions & 2 deletions src/classes/DateLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { enUS } from "date-fns/locale";
import { endOfBroadcastWeek } from "../helpers/endOfBroadcastWeek.js";
import { startOfBroadcastWeek } from "../helpers/startOfBroadcastWeek.js";
import type { PropsBase } from "../types/props.js";
import { Numerals } from "../types/shared.js";

export type { Locale } from "date-fns/locale";
export type { Month as DateFnsMonth } from "date-fns";
Expand Down Expand Up @@ -76,8 +77,18 @@ export interface DateLibOptions
Date?: typeof Date;
/** A locale to use for formatting dates. */
locale?: Locale;
/** A time zone to use for dates. */
/**
* A time zone to use for dates.
*
* @since 9.5.0
*/
timeZone?: string;
/**
* The numbering system to use for formatting numbers.
*
* @since 9.5.0
*/
numerals?: Numerals;
}

/**
Expand Down Expand Up @@ -111,6 +122,49 @@ export class DateLib {
this.overrides = overrides;
}

/**
* Generate digit map dynamically using Intl.NumberFormat.
*
* @since 9.5.0
*/
private getDigitMap(): Record<string, string> {
const { numerals = "latn" } = this.options;

// Use Intl.NumberFormat to create a formatter with the specified numbering system
const formatter = new Intl.NumberFormat("en-US", {
numberingSystem: numerals
});

// Map Arabic digits (0-9) to the target numerals
const digitMap: Record<string, string> = {};
for (let i = 0; i < 10; i++) {
digitMap[i.toString()] = formatter.format(i);
}

return digitMap;
}

/**
* Replace Arabic digits with the target numbering system digits.
*
* @since 9.5.0
*/
private replaceDigits(input: string): string {
const digitMap = this.getDigitMap();
return input.replace(/\d/g, (digit) => digitMap[digit] || digit);
}

/**
* Format number using the custom numbering system.
*
* @since 9.5.0
* @param value The number to format.
* @returns The formatted number.
*/
formatNumber(value: number): string {
return this.replaceDigits(value.toString());
}

/**
* Reference to the built-in Date constructor.
*
Expand Down Expand Up @@ -324,9 +378,13 @@ export class DateLib {
* @returns The formatted date string.
*/
format: typeof format = (date, formatStr) => {
return this.overrides?.format
const formatted = this.overrides?.format
? this.overrides.format(date, formatStr, this.options)
: format(date, formatStr, this.options);
if (this.options.numerals && this.options.numerals !== "latn") {
return this.replaceDigits(formatted);
}
return formatted;
};

/**
Expand Down
9 changes: 8 additions & 1 deletion src/types/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import type {
DayEventHandler,
Modifiers,
DateRange,
Mode
Mode,
Numerals
} from "./shared.js";

/**
Expand Down Expand Up @@ -390,6 +391,12 @@ export interface PropsBase {
* @see https://github.com/date-fns/date-fns/tree/main/src/locale for a list of the supported locales
*/
locale?: Partial<Locale> | undefined;
/**
* The numeral system to use when formatting dates.
*
* @see https://daypicker.dev/docs/translation#numeral-systems
*/
numerals?: Numerals;
/**
* The index of the first day of the week (0 - Sunday). Overrides the locale's
* one.
Expand Down
32 changes: 32 additions & 0 deletions src/types/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,3 +408,35 @@ export type MoveFocusBy =
| "endOfWeek"
| "month"
| "year";

/**
* The numbering system supported by DayPicker.
*
* - `latn`: Latin (Western Arabic)
* - `arab`: Arabic-Indic
* - `arabext`: Eastern Arabic-Indic (Persian)
* - `deva`: Devanagari
* - `beng`: Bengali
* - `guru`: Gurmukhi
* - `gujr`: Gujarati
* - `orya`: Oriya
* - `tamldec`: Tamil
* - `telu`: Telugu
* - `knda`: Kannada
* - `mlym`: Malayalam
*
* @see https://daypicker.dev/docs/translation#numeral-systems
*/
export type Numerals =
| "latn"
| "arab"
| "arabext"
| "deva"
| "beng"
| "guru"
| "gujr"
| "orya"
| "tamldec"
| "telu"
| "knda"
| "mlym";
58 changes: 18 additions & 40 deletions website/docs/docs/translation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,24 @@ import { arSA } from "react-day-picker/locale";
<Examples.Rtl />
</BrowserWindow>

## Numeral Systems

| Prop Name | Type | Description |
| ---------- | --------------------------------------------- | ----------------------- |
| `numerals` | [`Numerals`](../api/type-aliases/Numerals.md) | Set the numeral system. |

Use the `numerals` prop to set the numeral system. The default is `latn`.

```tsx
import { hi } from "date-fns/locale/hi";

<DayPicker numerals="deva" locale={hi} />;
```

<BrowserWindow>
<Examples.Numerals />
</BrowserWindow>

## Custom Formatters

Use the `formatters` prop to customize the formatting of dates, week numbers, day names, and more.
Expand All @@ -120,43 +138,3 @@ import { format } from "date-fns";
| [`formatWeekNumberHeader`](../api/functions/formatWeekNumberHeader.md) | Format the week number header. |
| [`formatWeekdayName`](../api/functions/formatWeekdayName.md) | Format the weekday name to be displayed in the weekdays header. |
| [`formatYearDropdown`](../api/functions/formatYearDropdown.md) | Format the years for the dropdown option label. |

### Numbering System

Use the `formatters` prop to change the [numbering system](https://en.wikipedia.org/wiki/Numeral_system) used in the calendar.

For example, to switch to Hindu-Arabic numerals, use the native [`Date.toLocaleString`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString) method:

```tsx
import { format } from "date-fns/format";
import { DayPicker, Formatters } from "react-day-picker";
import { arSA } from "react-day-picker/locale";

const NU_LOCALE = "ar-u-nu-arab";

const formatDay = (day) => day.getDate().toLocaleString(NU_LOCALE);
const formatWeekNumber = (weekNumber) => weekNumber.toLocaleString(NU_LOCALE);
const formatMonthCaption = (date, options) => {
const y = date.getFullYear().toLocaleString(NU_LOCALE);
const m = format(date, "LLLL", options);
return `${m} ${y}`;
};

export function NumberingSystemExample() {
return (
<DayPicker
locale={arSA}
dir="rtl"
formatters={{
formatDay,
formatMonthCaption,
formatWeekNumber
}}
/>
);
}
```

<BrowserWindow>
<Examples.NumberingSystem />
</BrowserWindow>
45 changes: 44 additions & 1 deletion website/src/pages/playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
type DayPickerProps,
DateLib,
DayPicker,
isDateRange
isDateRange,
Numerals
} from "react-day-picker";
import * as locales from "react-day-picker/locale";
import {
Expand All @@ -32,6 +33,21 @@ const timeZones = [

const calendars = ["Gregorian", "Persian"];
const persianLocales = { faIR: faIRPersian, enUS: enUSPersian };

const numerals: { value: Numerals; label: string }[] = [
{ value: "latn", label: "Latin (Western Arabic)" },
{ value: "arab", label: "Arabic-Indic" },
{ value: "arabext", label: "Eastern Arabic-Indic (Persian)" },
{ value: "deva", label: "Devanagari" },
{ value: "beng", label: "Bengali" },
{ value: "guru", label: "Gurmukhi" },
{ value: "gujr", label: "Gujarati" },
{ value: "orya", label: "Oriya" },
{ value: "tamldec", label: "Tamil" },
{ value: "telu", label: "Telugu" },
{ value: "knda", label: "Kannada" },
{ value: "mlym", label: "Malayalam" }
];
/**
* Function to format a json object of props to a jsx source displaying the
* props as example
Expand Down Expand Up @@ -421,6 +437,33 @@ export default function Playground() {
))}
</select>
</label>
<label>
Numerals:
<select
style={{ maxWidth: 100 }}
name="numerals"
value={
numerals.find((numeral) => numeral.value === props.numerals)
?.value
}
onChange={(e) =>
setProps({
...props,
numerals:
e.target.value === ""
? undefined
: (e.target.value as Numerals)
})
}
>
<option value=""></option>
{numerals.map((numeral) => (
<option key={numeral.value} value={numeral.value}>
{numeral.label}
</option>
))}
</select>
</label>
<label>
Locale:
{calendar === "Persian" ? (
Expand Down

0 comments on commit d09e2ac

Please sign in to comment.