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 shouldRenderYears = forcePresentation !== 'month' && forcePresentation !== 'time';
|
||||||
const years = shouldRenderYears
|
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 { day, dayOfWeek } = dateObject;
|
||||||
const { isDateEnabled, multiple } = this;
|
const { isDateEnabled, multiple } = this;
|
||||||
const referenceParts = { month, day, year };
|
const referenceParts = { month, day, year };
|
||||||
const { isActive, isToday, ariaLabel, ariaSelected, disabled } = getCalendarDayState(
|
const { isActive, isToday, ariaLabel, ariaSelected, disabled, text } = getCalendarDayState(
|
||||||
this.locale,
|
this.locale,
|
||||||
referenceParts,
|
referenceParts,
|
||||||
this.activePartsClone,
|
this.activePartsClone,
|
||||||
@ -1987,7 +1987,7 @@ export class Datetime implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{day}
|
{text}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -62,6 +62,26 @@ test.describe('datetime: locale', () => {
|
|||||||
test('time picker should not have visual regressions', async () => {
|
test('time picker should not have visual regressions', async () => {
|
||||||
await datetimeFixture.expectLocalizedTimePicker();
|
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', () => {
|
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 {
|
class DatetimeLocaleFixture {
|
||||||
readonly page: E2EPage;
|
readonly page: E2EPage;
|
||||||
locale = 'en-US';
|
locale = 'en-US';
|
||||||
|
|||||||
@ -12,6 +12,7 @@ describe('getCalendarDayState()', () => {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
ariaSelected: null,
|
ariaSelected: null,
|
||||||
ariaLabel: 'Tuesday, January 1',
|
ariaLabel: 'Tuesday, January 1',
|
||||||
|
text: '1',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getCalendarDayState('en-US', refA, refA, refC)).toEqual({
|
expect(getCalendarDayState('en-US', refA, refA, refC)).toEqual({
|
||||||
@ -20,6 +21,7 @@ describe('getCalendarDayState()', () => {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
ariaSelected: 'true',
|
ariaSelected: 'true',
|
||||||
ariaLabel: 'Tuesday, January 1',
|
ariaLabel: 'Tuesday, January 1',
|
||||||
|
text: '1',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getCalendarDayState('en-US', refA, refB, refA)).toEqual({
|
expect(getCalendarDayState('en-US', refA, refB, refA)).toEqual({
|
||||||
@ -28,6 +30,7 @@ describe('getCalendarDayState()', () => {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
ariaSelected: null,
|
ariaSelected: null,
|
||||||
ariaLabel: 'Today, Tuesday, January 1',
|
ariaLabel: 'Today, Tuesday, January 1',
|
||||||
|
text: '1',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getCalendarDayState('en-US', refA, refA, refA)).toEqual({
|
expect(getCalendarDayState('en-US', refA, refA, refA)).toEqual({
|
||||||
@ -36,6 +39,7 @@ describe('getCalendarDayState()', () => {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
ariaSelected: 'true',
|
ariaSelected: 'true',
|
||||||
ariaLabel: 'Today, Tuesday, January 1',
|
ariaLabel: 'Today, Tuesday, January 1',
|
||||||
|
text: '1',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getCalendarDayState('en-US', refA, refA, refA, undefined, undefined, [1])).toEqual({
|
expect(getCalendarDayState('en-US', refA, refA, refA, undefined, undefined, [1])).toEqual({
|
||||||
@ -44,6 +48,7 @@ describe('getCalendarDayState()', () => {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
ariaSelected: 'true',
|
ariaSelected: 'true',
|
||||||
ariaLabel: 'Today, Tuesday, January 1',
|
ariaLabel: 'Today, Tuesday, January 1',
|
||||||
|
text: '1',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getCalendarDayState('en-US', refA, refA, refA, undefined, undefined, [2])).toEqual({
|
expect(getCalendarDayState('en-US', refA, refA, refA, undefined, undefined, [2])).toEqual({
|
||||||
@ -52,6 +57,7 @@ describe('getCalendarDayState()', () => {
|
|||||||
disabled: true,
|
disabled: true,
|
||||||
ariaSelected: 'true',
|
ariaSelected: 'true',
|
||||||
ariaLabel: 'Today, Tuesday, January 1',
|
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 type { DatetimeParts } from '../datetime-interface';
|
||||||
|
|
||||||
import { isAfter, isBefore, isSameDay } from './comparison';
|
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 { getNumDaysInMonth, is24Hour } from './helpers';
|
||||||
import { getNextMonth, getPreviousMonth, getInternalHourValue } from './manipulation';
|
import { getNextMonth, getPreviousMonth, getInternalHourValue } from './manipulation';
|
||||||
|
|
||||||
@ -378,6 +385,7 @@ export const getDayColumnData = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getYearColumnData = (
|
export const getYearColumnData = (
|
||||||
|
locale: string,
|
||||||
refParts: DatetimeParts,
|
refParts: DatetimeParts,
|
||||||
minParts?: DatetimeParts,
|
minParts?: DatetimeParts,
|
||||||
maxParts?: DatetimeParts,
|
maxParts?: DatetimeParts,
|
||||||
@ -403,7 +411,7 @@ export const getYearColumnData = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return processedYears.map((year) => ({
|
return processedYears.map((year) => ({
|
||||||
text: `${year}`,
|
text: getYear(locale, { year, month: refParts.month, day: refParts.day }),
|
||||||
value: year,
|
value: year,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|||||||
@ -119,19 +119,73 @@ export const getMonthDayAndYear = (locale: string, refParts: DatetimeParts) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper function for Intl.DateTimeFormat.
|
* Given a locale and a date object,
|
||||||
* Allows developers to apply an allowed format to DatetimeParts.
|
* return a formatted string that includes
|
||||||
* This function also has built in safeguards for older browser bugs
|
* the numeric day.
|
||||||
* with Intl.DateTimeFormat.
|
* 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 = (
|
export const getLocalizedDateTime = (
|
||||||
locale: string,
|
locale: string,
|
||||||
refParts: DatetimeParts,
|
refParts: DatetimeParts,
|
||||||
options: Intl.DateTimeFormatOptions
|
options: Intl.DateTimeFormatOptions
|
||||||
): string => {
|
): string => {
|
||||||
const timeString = !!refParts.hour && !!refParts.minute ? ` ${refParts.hour}:${refParts.minute}` : '';
|
const date = getNormalizedDate(refParts);
|
||||||
const date = new Date(`${refParts.month}/${refParts.day}/${refParts.year}${timeString} GMT+0000`);
|
return getDateTimeFormat(locale, options).format(date);
|
||||||
return new Intl.DateTimeFormat(locale, { ...options, timeZone: 'UTC' }).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 type { DatetimeParts } from '../datetime-interface';
|
||||||
|
|
||||||
import { isAfter, isBefore, isSameDay } from './comparison';
|
import { isAfter, isBefore, isSameDay } from './comparison';
|
||||||
import { generateDayAriaLabel } from './format';
|
import { generateDayAriaLabel, getDay } from './format';
|
||||||
import { getNextMonth, getPreviousMonth } from './manipulation';
|
import { getNextMonth, getPreviousMonth } from './manipulation';
|
||||||
|
|
||||||
export const isYearDisabled = (refYear: number, minParts?: DatetimeParts, maxParts?: DatetimeParts) => {
|
export const isYearDisabled = (refYear: number, minParts?: DatetimeParts, maxParts?: DatetimeParts) => {
|
||||||
@ -123,6 +123,7 @@ export const getCalendarDayState = (
|
|||||||
isToday,
|
isToday,
|
||||||
ariaSelected: isActive ? 'true' : null,
|
ariaSelected: isActive ? 'true' : null,
|
||||||
ariaLabel: generateDayAriaLabel(locale, isToday, refParts),
|
ariaLabel: generateDayAriaLabel(locale, isToday, refParts),
|
||||||
|
text: refParts.day != null ? getDay(locale, refParts) : null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user