fix(datetime): calendar day and years are now localized (#25847)

resolves #25843
This commit is contained in:
Liam DeBeasi
2022-09-01 11:54:10 -05:00
committed by GitHub
parent c11f509eb4
commit cbd1268a03
6 changed files with 136 additions and 13 deletions

View File

@ -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>
);
})}

View File

@ -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';

View File

@ -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',
});
});
});

View File

@ -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,
}));
};

View File

@ -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' });
};
/**

View File

@ -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,
};
};