Skip to content

Commit

Permalink
fix(datetime): calendar day and years are now localized (#25847)
Browse files Browse the repository at this point in the history
resolves #25843
  • Loading branch information
liamdebeasi authored Sep 1, 2022
1 parent c11f509 commit cbd1268
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 13 deletions.
6 changes: 3 additions & 3 deletions core/src/components/datetime/datetime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
: [];

/**
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1987,7 +1987,7 @@ export class Datetime implements ComponentInterface {
}
}}
>
{day}
{text}
</button>
);
})}
Expand Down
54 changes: 54 additions & 0 deletions core/src/components/datetime/test/locale/datetime.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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';
Expand Down
6 changes: 6 additions & 0 deletions core/src/components/datetime/test/state.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('getCalendarDayState()', () => {
disabled: false,
ariaSelected: null,
ariaLabel: 'Tuesday, January 1',
text: '1',
});

expect(getCalendarDayState('en-US', refA, refA, refC)).toEqual({
Expand All @@ -20,6 +21,7 @@ describe('getCalendarDayState()', () => {
disabled: false,
ariaSelected: 'true',
ariaLabel: 'Tuesday, January 1',
text: '1',
});

expect(getCalendarDayState('en-US', refA, refB, refA)).toEqual({
Expand All @@ -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({
Expand All @@ -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({
Expand All @@ -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({
Expand All @@ -52,6 +57,7 @@ describe('getCalendarDayState()', () => {
disabled: true,
ariaSelected: 'true',
ariaLabel: 'Today, Tuesday, January 1',
text: '1',
});
});
});
Expand Down
12 changes: 10 additions & 2 deletions core/src/components/datetime/utils/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -378,6 +385,7 @@ export const getDayColumnData = (
};

export const getYearColumnData = (
locale: string,
refParts: DatetimeParts,
minParts?: DatetimeParts,
maxParts?: DatetimeParts,
Expand All @@ -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,
}));
};
Expand Down
68 changes: 61 additions & 7 deletions core/src/components/datetime/utils/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
};

/**
Expand Down
3 changes: 2 additions & 1 deletion core/src/components/datetime/utils/state.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down Expand Up @@ -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,
};
};

Expand Down

0 comments on commit cbd1268

Please sign in to comment.