diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx
index a8c9d46ab6..bb8d0bad26 100644
--- a/core/src/components/datetime/datetime.tsx
+++ b/core/src/components/datetime/datetime.tsx
@@ -1545,7 +1545,7 @@ export class Datetime implements ComponentInterface {
const shouldRenderYears = forcePresentation !== 'month' && forcePresentation !== 'time';
const years = shouldRenderYears
- ? getYearColumnData(this.todayParts, this.minParts, this.maxParts, this.parsedYearValues)
+ ? getYearColumnData(this.locale, this.todayParts, this.minParts, this.maxParts, this.parsedYearValues)
: [];
/**
@@ -1910,7 +1910,7 @@ export class Datetime implements ComponentInterface {
const { day, dayOfWeek } = dateObject;
const { isDateEnabled, multiple } = this;
const referenceParts = { month, day, year };
- const { isActive, isToday, ariaLabel, ariaSelected, disabled } = getCalendarDayState(
+ const { isActive, isToday, ariaLabel, ariaSelected, disabled, text } = getCalendarDayState(
this.locale,
referenceParts,
this.activePartsClone,
@@ -1987,7 +1987,7 @@ export class Datetime implements ComponentInterface {
}
}}
>
- {day}
+ {text}
);
})}
diff --git a/core/src/components/datetime/test/locale/datetime.e2e.ts b/core/src/components/datetime/test/locale/datetime.e2e.ts
index a15a23820d..aaee56f5ac 100644
--- a/core/src/components/datetime/test/locale/datetime.e2e.ts
+++ b/core/src/components/datetime/test/locale/datetime.e2e.ts
@@ -62,6 +62,26 @@ test.describe('datetime: locale', () => {
test('time picker should not have visual regressions', async () => {
await datetimeFixture.expectLocalizedTimePicker();
});
+
+ test('should correctly localize calendar day buttons without literal', async ({ page }) => {
+ await page.setContent(`
+
+ `);
+
+ await page.waitForSelector('.datetime-ready');
+
+ const datetimeButtons = page.locator('ion-datetime .calendar-day:not([disabled])');
+
+ /**
+ * Note: The Intl.DateTimeFormat typically adds literals
+ * for certain languages. For Japanese, that could look
+ * something like "29日". However, we only want the "29"
+ * to be shown.
+ */
+ await expect(datetimeButtons.nth(0)).toHaveText('1');
+ await expect(datetimeButtons.nth(1)).toHaveText('2');
+ await expect(datetimeButtons.nth(2)).toHaveText('3');
+ });
});
test.describe('es-ES', () => {
@@ -83,6 +103,40 @@ test.describe('datetime: locale', () => {
});
});
+test.describe('ar-EG', () => {
+ test.beforeEach(async ({ skip }) => {
+ skip.rtl();
+ skip.mode('md');
+ });
+
+ test('should correctly localize calendar day buttons', async ({ page }) => {
+ await page.setContent(`
+
+ `);
+
+ await page.waitForSelector('.datetime-ready');
+
+ const datetimeButtons = page.locator('ion-datetime .calendar-day:not([disabled])');
+
+ await expect(datetimeButtons.nth(0)).toHaveText('١');
+ await expect(datetimeButtons.nth(1)).toHaveText('٢');
+ await expect(datetimeButtons.nth(2)).toHaveText('٣');
+ });
+
+ test('should correctly localize year column data', async ({ page }) => {
+ await page.setContent(`
+
+ `);
+ await page.waitForSelector('.datetime-ready');
+
+ const datetimeYears = page.locator('ion-datetime .year-column .picker-item:not(.picker-item-empty)');
+
+ await expect(datetimeYears.nth(0)).toHaveText('٢٠٢٢');
+ await expect(datetimeYears.nth(1)).toHaveText('٢٠٢١');
+ await expect(datetimeYears.nth(2)).toHaveText('٢٠٢٠');
+ });
+});
+
class DatetimeLocaleFixture {
readonly page: E2EPage;
locale = 'en-US';
diff --git a/core/src/components/datetime/test/state.spec.ts b/core/src/components/datetime/test/state.spec.ts
index a6c35425c1..71cc968c8b 100644
--- a/core/src/components/datetime/test/state.spec.ts
+++ b/core/src/components/datetime/test/state.spec.ts
@@ -12,6 +12,7 @@ describe('getCalendarDayState()', () => {
disabled: false,
ariaSelected: null,
ariaLabel: 'Tuesday, January 1',
+ text: '1',
});
expect(getCalendarDayState('en-US', refA, refA, refC)).toEqual({
@@ -20,6 +21,7 @@ describe('getCalendarDayState()', () => {
disabled: false,
ariaSelected: 'true',
ariaLabel: 'Tuesday, January 1',
+ text: '1',
});
expect(getCalendarDayState('en-US', refA, refB, refA)).toEqual({
@@ -28,6 +30,7 @@ describe('getCalendarDayState()', () => {
disabled: false,
ariaSelected: null,
ariaLabel: 'Today, Tuesday, January 1',
+ text: '1',
});
expect(getCalendarDayState('en-US', refA, refA, refA)).toEqual({
@@ -36,6 +39,7 @@ describe('getCalendarDayState()', () => {
disabled: false,
ariaSelected: 'true',
ariaLabel: 'Today, Tuesday, January 1',
+ text: '1',
});
expect(getCalendarDayState('en-US', refA, refA, refA, undefined, undefined, [1])).toEqual({
@@ -44,6 +48,7 @@ describe('getCalendarDayState()', () => {
disabled: false,
ariaSelected: 'true',
ariaLabel: 'Today, Tuesday, January 1',
+ text: '1',
});
expect(getCalendarDayState('en-US', refA, refA, refA, undefined, undefined, [2])).toEqual({
@@ -52,6 +57,7 @@ describe('getCalendarDayState()', () => {
disabled: true,
ariaSelected: 'true',
ariaLabel: 'Today, Tuesday, January 1',
+ text: '1',
});
});
});
diff --git a/core/src/components/datetime/utils/data.ts b/core/src/components/datetime/utils/data.ts
index ed11abc040..073541dbe1 100644
--- a/core/src/components/datetime/utils/data.ts
+++ b/core/src/components/datetime/utils/data.ts
@@ -3,7 +3,14 @@ import type { PickerColumnItem } from '../../picker-column-internal/picker-colum
import type { DatetimeParts } from '../datetime-interface';
import { isAfter, isBefore, isSameDay } from './comparison';
-import { getLocalizedDayPeriod, removeDateTzOffset, getFormattedHour, addTimePadding, getTodayLabel } from './format';
+import {
+ getLocalizedDayPeriod,
+ removeDateTzOffset,
+ getFormattedHour,
+ addTimePadding,
+ getTodayLabel,
+ getYear,
+} from './format';
import { getNumDaysInMonth, is24Hour } from './helpers';
import { getNextMonth, getPreviousMonth, getInternalHourValue } from './manipulation';
@@ -378,6 +385,7 @@ export const getDayColumnData = (
};
export const getYearColumnData = (
+ locale: string,
refParts: DatetimeParts,
minParts?: DatetimeParts,
maxParts?: DatetimeParts,
@@ -403,7 +411,7 @@ export const getYearColumnData = (
}
return processedYears.map((year) => ({
- text: `${year}`,
+ text: getYear(locale, { year, month: refParts.month, day: refParts.day }),
value: year,
}));
};
diff --git a/core/src/components/datetime/utils/format.ts b/core/src/components/datetime/utils/format.ts
index 3869d376b4..2ea4980381 100644
--- a/core/src/components/datetime/utils/format.ts
+++ b/core/src/components/datetime/utils/format.ts
@@ -119,19 +119,73 @@ export const getMonthDayAndYear = (locale: string, refParts: DatetimeParts) => {
};
/**
- * Wrapper function for Intl.DateTimeFormat.
- * Allows developers to apply an allowed format to DatetimeParts.
- * This function also has built in safeguards for older browser bugs
- * with Intl.DateTimeFormat.
+ * Given a locale and a date object,
+ * return a formatted string that includes
+ * the numeric day.
+ * Note: Some languages will add literal characters
+ * to the end. This function removes those literals.
+ * Example: 29
+ */
+export const getDay = (locale: string, refParts: DatetimeParts) => {
+ return getLocalizedDateTimeParts(locale, refParts, { day: 'numeric' }).find((obj) => obj.type === 'day')!.value;
+};
+
+/**
+ * Given a locale and a date object,
+ * return a formatted string that includes
+ * the numeric year.
+ * Example: 2022
+ */
+export const getYear = (locale: string, refParts: DatetimeParts) => {
+ return getLocalizedDateTime(locale, refParts, { year: 'numeric' });
+};
+
+const getNormalizedDate = (refParts: DatetimeParts) => {
+ const timeString = !!refParts.hour && !!refParts.minute ? ` ${refParts.hour}:${refParts.minute}` : '';
+
+ return new Date(`${refParts.month}/${refParts.day}/${refParts.year}${timeString} GMT+0000`);
+};
+
+/**
+ * Given a locale, DatetimeParts, and options
+ * format the DatetimeParts according to the options
+ * and locale combination. This returns a string. If
+ * you want an array of the individual pieces
+ * that make up the localized date string, use
+ * getLocalizedDateTimeParts.
*/
export const getLocalizedDateTime = (
locale: string,
refParts: DatetimeParts,
options: Intl.DateTimeFormatOptions
): string => {
- const timeString = !!refParts.hour && !!refParts.minute ? ` ${refParts.hour}:${refParts.minute}` : '';
- const date = new Date(`${refParts.month}/${refParts.day}/${refParts.year}${timeString} GMT+0000`);
- return new Intl.DateTimeFormat(locale, { ...options, timeZone: 'UTC' }).format(date);
+ const date = getNormalizedDate(refParts);
+ return getDateTimeFormat(locale, options).format(date);
+};
+
+/**
+ * Given a locale, DatetimeParts, and options
+ * format the DatetimeParts according to the options
+ * and locale combination. This returns an array of
+ * each piece of the date.
+ */
+export const getLocalizedDateTimeParts = (
+ locale: string,
+ refParts: DatetimeParts,
+ options: Intl.DateTimeFormatOptions
+): Intl.DateTimeFormatPart[] => {
+ const date = getNormalizedDate(refParts);
+ return getDateTimeFormat(locale, options).formatToParts(date);
+};
+
+/**
+ * Wrapper function for Intl.DateTimeFormat.
+ * Allows developers to apply an allowed format to DatetimeParts.
+ * This function also has built in safeguards for older browser bugs
+ * with Intl.DateTimeFormat.
+ */
+const getDateTimeFormat = (locale: string, options: Intl.DateTimeFormatOptions) => {
+ return new Intl.DateTimeFormat(locale, { ...options, timeZone: 'UTC' });
};
/**
diff --git a/core/src/components/datetime/utils/state.ts b/core/src/components/datetime/utils/state.ts
index 1ad345d4e1..0d9200ffeb 100644
--- a/core/src/components/datetime/utils/state.ts
+++ b/core/src/components/datetime/utils/state.ts
@@ -1,7 +1,7 @@
import type { DatetimeParts } from '../datetime-interface';
import { isAfter, isBefore, isSameDay } from './comparison';
-import { generateDayAriaLabel } from './format';
+import { generateDayAriaLabel, getDay } from './format';
import { getNextMonth, getPreviousMonth } from './manipulation';
export const isYearDisabled = (refYear: number, minParts?: DatetimeParts, maxParts?: DatetimeParts) => {
@@ -123,6 +123,7 @@ export const getCalendarDayState = (
isToday,
ariaSelected: isActive ? 'true' : null,
ariaLabel: generateDayAriaLabel(locale, isToday, refParts),
+ text: refParts.day != null ? getDay(locale, refParts) : null,
};
};