mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-08 15:51:16 +08:00
fix(datetime): calendar day and years are now localized (#25847)
resolves #25843
This commit is contained in:
@ -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}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -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(`
|
||||
<ion-datetime locale="ja-JP" presentation="date" value="2022-01-01"></ion-datetime>
|
||||
`);
|
||||
|
||||
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(`
|
||||
<ion-datetime locale="ar-EG" presentation="date" value="2022-01-01"></ion-datetime>
|
||||
`);
|
||||
|
||||
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(`
|
||||
<ion-datetime prefer-wheel="true" locale="ar-EG" presentation="date" value="2022-01-01"></ion-datetime>
|
||||
`);
|
||||
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';
|
||||
|
||||
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,
|
||||
}));
|
||||
};
|
||||
|
||||
@ -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' });
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user