From 44f07aaaa6cf71b0552a2a8d52904db8c2463a59 Mon Sep 17 00:00:00 2001 From: JYC Date: Wed, 10 Jan 2024 00:02:27 +0100 Subject: [PATCH] Format last qol for next (#203) * add WeekStartsOn defaults depending on the locale * init a Language Select comp --- .../src/lib/components/LanguageSelect.svelte | 49 +++++++++++ packages/svelte-ux/src/lib/utils/date.test.ts | 74 ++++++++++------ packages/svelte-ux/src/lib/utils/date.ts | 74 +++++++++------- .../svelte-ux/src/lib/utils/dateInternal.ts | 11 +++ packages/svelte-ux/src/lib/utils/dateRange.ts | 3 +- .../svelte-ux/src/lib/utils/date_types.ts | 12 +-- packages/svelte-ux/src/lib/utils/format.ts | 12 +-- packages/svelte-ux/src/lib/utils/index.ts | 3 +- packages/svelte-ux/src/lib/utils/locale.ts | 21 +++++ packages/svelte-ux/src/routes/+layout.svelte | 87 +++++++------------ 10 files changed, 211 insertions(+), 135 deletions(-) create mode 100644 packages/svelte-ux/src/lib/components/LanguageSelect.svelte create mode 100644 packages/svelte-ux/src/lib/utils/dateInternal.ts diff --git a/packages/svelte-ux/src/lib/components/LanguageSelect.svelte b/packages/svelte-ux/src/lib/components/LanguageSelect.svelte new file mode 100644 index 000000000..0e388caec --- /dev/null +++ b/packages/svelte-ux/src/lib/components/LanguageSelect.svelte @@ -0,0 +1,49 @@ + + + diff --git a/packages/svelte-ux/src/lib/utils/date.test.ts b/packages/svelte-ux/src/lib/utils/date.test.ts index a40019d7f..3caa232fe 100644 --- a/packages/svelte-ux/src/lib/utils/date.test.ts +++ b/packages/svelte-ux/src/lib/utils/date.test.ts @@ -1,25 +1,28 @@ import { describe, it, expect } from 'vitest'; import { - PeriodType, formatDate, getMonthDaysByWeek, localToUtcDate, utcToLocalDate, - DayOfWeek, formatIntl, - type CustomIntlDateTimeFormatOptions, - type FormatDateOptions, - DateToken, formatDateWithLocale, } from './date'; -import { getSettings } from '$lib/components'; -import { format, formatWithLocale } from '.'; +import { formatWithLocale } from '.'; import { createLocaleSettings, defaultLocale } from './locale'; +import { + PeriodType, + type FormatDateOptions, + DayOfWeek, + type CustomIntlDateTimeFormatOptions, + DateToken, +} from './date_types'; +import { getWeekStartsOnFromIntl } from './dateInternal'; const DATE = '2023-11-21'; // "good" default date as the day (21) is bigger than 12 (number of months). And november is a good month1 (because why not?) const dt_2M_2d = new Date(2023, 10, 21); const dt_2M_1d = new Date(2023, 10, 7); const dt_1M_1d = new Date(2023, 2, 7); +const dt_first = new Date(2024, 1, 1); const dt_1M_1d_time_pm = new Date(2023, 2, 7, 14, 2, 3, 4); const dt_1M_1d_time_am = new Date(2023, 2, 7, 1, 2, 3, 4); @@ -30,9 +33,6 @@ const fr = createLocaleSettings({ dates: { ordinalSuffixes: { one: 'er', - two: '', - few: '', - other: '', }, }, }, @@ -40,11 +40,14 @@ const fr = createLocaleSettings({ describe('formatDate()', () => { it('should return empty string for null or undefined date', () => { + // @ts-expect-error expect(formatDate(null)).equal(''); + // @ts-expect-error expect(formatDate(undefined)).equal(''); }); it('should return empty string for invalid date', () => { + // @ts-expect-error expect(formatDate('invalid date')).equal(''); }); @@ -125,7 +128,7 @@ describe('formatDate()', () => { } }); - describe('should format date for PeriodType.WeekSun / Mon', () => { + describe('should format date for PeriodType.WeekSun / Mon no mather the locale', () => { const combi = [ [PeriodType.WeekSun, 'short', defaultLocale, '11/19 - 11/25'], [PeriodType.WeekSun, 'short', fr, '19/11 - 25/11'], @@ -138,32 +141,28 @@ describe('formatDate()', () => { for (const c of combi) { const [periodType, variant, locales, expected] = c; it(c.toString(), () => { - expect(formatDateWithLocale(locales, DATE, periodType, { variant, locales })).equal( - expected - ); + expect(formatDateWithLocale(locales, DATE, periodType, { variant })).equal(expected); }); } }); - describe('should format date for PeriodType.Week', () => { + describe('should format date for PeriodType.Week with the good weekstarton locale', () => { const combi = [ - [PeriodType.Week, 'short', defaultLocale, DayOfWeek.Sunday, '11/19 - 11/25'], - [PeriodType.Week, 'short', fr, DayOfWeek.Sunday, '19/11 - 25/11'], - [PeriodType.Week, 'long', defaultLocale, DayOfWeek.Sunday, '11/19/2023 - 11/25/2023'], - [PeriodType.Week, 'long', fr, DayOfWeek.Sunday, '19/11/2023 - 25/11/2023'], - - [PeriodType.Week, 'short', defaultLocale, DayOfWeek.Monday, '11/20 - 11/26'], - [PeriodType.Week, 'short', fr, DayOfWeek.Monday, '20/11 - 26/11'], - [PeriodType.Week, 'long', defaultLocale, DayOfWeek.Monday, '11/20/2023 - 11/26/2023'], - [PeriodType.Week, 'long', fr, DayOfWeek.Monday, '20/11/2023 - 26/11/2023'], + [PeriodType.Week, 'short', defaultLocale, '11/19 - 11/25'], + [PeriodType.Week, 'short', fr, '20/11 - 26/11'], + [PeriodType.Week, 'long', defaultLocale, '11/19/2023 - 11/25/2023'], + [PeriodType.Week, 'long', fr, '20/11/2023 - 26/11/2023'], + + [PeriodType.Week, 'short', defaultLocale, '11/19 - 11/25'], + [PeriodType.Week, 'short', fr, '20/11 - 26/11'], + [PeriodType.Week, 'long', defaultLocale, '11/19/2023 - 11/25/2023'], + [PeriodType.Week, 'long', fr, '20/11/2023 - 26/11/2023'], ] as const; for (const c of combi) { - const [periodType, variant, locales, weekStartsOn, expected] = c; + const [periodType, variant, locales, expected] = c; it(c.toString(), () => { - expect(formatDateWithLocale(locales, DATE, periodType, { variant, weekStartsOn })).equal( - expected - ); + expect(formatDateWithLocale(locales, DATE, periodType, { variant })).equal(expected); }); } }); @@ -286,6 +285,7 @@ describe('formatDate()', () => { for (const c of combi) { const [variant, locales] = c; it(c.toString(), () => { + // @ts-expect-error expect(formatDateWithLocale(locales, DATE, undefined, { variant })).equal(expected); }); } @@ -316,6 +316,7 @@ describe('formatIntl() tokens', () => { [dt_2M_2d, { dateStyle: 'medium', withOrdinal: true }, ['Nov 21st, 2023', '21 nov. 2023']], [dt_2M_2d, { dateStyle: 'short' }, ['11/21/23', '21/11/2023']], [dt_1M_1d, { dateStyle: 'short' }, ['3/7/23', '07/03/2023']], + [dt_first, DateToken.DayOfMonth_withOrdinal, ['1st', '1er']], // time [dt_1M_1d_time_pm, [DateToken.Hour_numeric, DateToken.Minute_numeric], ['2:02 PM', '14:02']], @@ -500,3 +501,20 @@ describe('getMonthDaysByWeek()', () => { `); }); }); + +describe('getWeekStartsOnFromIntl() tokens', () => { + it('by default, sunday', () => { + const val = getWeekStartsOnFromIntl(); + expect(val).toBe(DayOfWeek.Sunday); + }); + + it('For en it should be synday', () => { + const val = getWeekStartsOnFromIntl('en'); + expect(val).toBe(DayOfWeek.Sunday); + }); + + it('For fr it should be monday', () => { + const val = getWeekStartsOnFromIntl('fr'); + expect(val).toBe(DayOfWeek.Monday); + }); +}); diff --git a/packages/svelte-ux/src/lib/utils/date.ts b/packages/svelte-ux/src/lib/utils/date.ts index 673acf6fd..5c41116d3 100644 --- a/packages/svelte-ux/src/lib/utils/date.ts +++ b/packages/svelte-ux/src/lib/utils/date.ts @@ -32,8 +32,15 @@ import { import { hasKeyOf } from '../types/typeGuards'; import { chunk } from './array'; -import type { DateRange } from './dateRange'; -import { PeriodType, DayOfWeek, DateToken } from './date_types'; +import { + PeriodType, + DayOfWeek, + DateToken, + type SelectedDate, + type CustomIntlDateTimeFormatOptions, + type FormatDateOptions, + type DateFormatVariantPreset, +} from './date_types'; import { defaultLocale, type LocaleSettings } from './locale'; export * from './date_types'; @@ -676,6 +683,7 @@ export function formatDateWithLocale( } const weekStartsOn = options.weekStartsOn ?? settings.formats.dates.weekStartsOn; + const { day, dayTime, timeOnly, week, month, monthsYear, year } = settings.formats.dates.presets; if (periodType === PeriodType.Week) { @@ -723,80 +731,80 @@ export function formatDateWithLocale( switch (periodType) { case PeriodType.Custom: - return formatIntl(settings, date, options.custom); + return formatIntl(settings, date, options.custom!); case PeriodType.Day: - return formatIntl(settings, date, rv(day)); + return formatIntl(settings, date, rv(day!)!); case PeriodType.DayTime: - return formatIntl(settings, date, rv(dayTime)); + return formatIntl(settings, date, rv(dayTime!)!); case PeriodType.TimeOnly: - return formatIntl(settings, date, rv(timeOnly)); + return formatIntl(settings, date, rv(timeOnly!)!); case PeriodType.WeekSun: - return range(settings, date, 0, rv(week)); + return range(settings, date, 0, rv(week!)!); case PeriodType.WeekMon: - return range(settings, date, 1, rv(week)); + return range(settings, date, 1, rv(week!)!); case PeriodType.WeekTue: - return range(settings, date, 2, rv(week)); + return range(settings, date, 2, rv(week!)!); case PeriodType.WeekWed: - return range(settings, date, 3, rv(week)); + return range(settings, date, 3, rv(week!)!); case PeriodType.WeekThu: - return range(settings, date, 4, rv(week)); + return range(settings, date, 4, rv(week!)!); case PeriodType.WeekFri: - return range(settings, date, 5, rv(week)); + return range(settings, date, 5, rv(week!)!); case PeriodType.WeekSat: - return range(settings, date, 6, rv(week)); + return range(settings, date, 6, rv(week!)!); case PeriodType.Month: - return formatIntl(settings, date, rv(month)); + return formatIntl(settings, date, rv(month!)!); case PeriodType.MonthYear: - return formatIntl(settings, date, rv(monthsYear)); + return formatIntl(settings, date, rv(monthsYear!)!); case PeriodType.Quarter: return [ - formatIntl(settings, startOfQuarter(date), rv(month)), - formatIntl(settings, endOfQuarter(date), rv(monthsYear)), + formatIntl(settings, startOfQuarter(date), rv(month!)!), + formatIntl(settings, endOfQuarter(date), rv(monthsYear!)!), ].join(' - '); case PeriodType.CalendarYear: - return formatIntl(settings, date, rv(year)); + return formatIntl(settings, date, rv(year!)!); case PeriodType.FiscalYearOctober: const fDate = new Date(getFiscalYear(date), 0, 1); - return formatIntl(settings, fDate, rv(year)); + return formatIntl(settings, fDate, rv(year!)!); case PeriodType.BiWeek1Sun: - return range(settings, date, 0, rv(week), 1); + return range(settings, date, 0, rv(week!)!, 1); case PeriodType.BiWeek1Mon: - return range(settings, date, 1, rv(week), 1); + return range(settings, date, 1, rv(week!)!, 1); case PeriodType.BiWeek1Tue: - return range(settings, date, 2, rv(week), 1); + return range(settings, date, 2, rv(week!)!, 1); case PeriodType.BiWeek1Wed: - return range(settings, date, 3, rv(week), 1); + return range(settings, date, 3, rv(week!)!, 1); case PeriodType.BiWeek1Thu: - return range(settings, date, 4, rv(week), 1); + return range(settings, date, 4, rv(week!)!, 1); case PeriodType.BiWeek1Fri: - return range(settings, date, 5, rv(week), 1); + return range(settings, date, 5, rv(week!)!, 1); case PeriodType.BiWeek1Sat: - return range(settings, date, 6, rv(week), 1); + return range(settings, date, 6, rv(week!)!, 1); case PeriodType.BiWeek2Sun: - return range(settings, date, 0, rv(week), 2); + return range(settings, date, 0, rv(week!)!, 2); case PeriodType.BiWeek2Mon: - return range(settings, date, 1, rv(week), 2); + return range(settings, date, 1, rv(week!)!, 2); case PeriodType.BiWeek2Tue: - return range(settings, date, 2, rv(week), 2); + return range(settings, date, 2, rv(week!)!, 2); case PeriodType.BiWeek2Wed: - return range(settings, date, 3, rv(week), 2); + return range(settings, date, 3, rv(week!)!, 2); case PeriodType.BiWeek2Thu: - return range(settings, date, 4, rv(week), 2); + return range(settings, date, 4, rv(week!)!, 2); case PeriodType.BiWeek2Fri: - return range(settings, date, 5, rv(week), 2); + return range(settings, date, 5, rv(week!)!, 2); case PeriodType.BiWeek2Sat: - return range(settings, date, 6, rv(week), 2); + return range(settings, date, 6, rv(week!)!, 2); default: return formatISO(date); diff --git a/packages/svelte-ux/src/lib/utils/dateInternal.ts b/packages/svelte-ux/src/lib/utils/dateInternal.ts new file mode 100644 index 000000000..4dafad8b1 --- /dev/null +++ b/packages/svelte-ux/src/lib/utils/dateInternal.ts @@ -0,0 +1,11 @@ +import { DayOfWeek } from './date_types'; + +export function getWeekStartsOnFromIntl(locales?: string): DayOfWeek { + if (!locales) { + return DayOfWeek.Sunday; + } + + const info = new Intl.Locale(locales); + // @ts-ignore + return (info.weekInfo.firstDay ?? 0) % 7; // (in Intl, sunday is 7 not 0, so we need to mod 7) +} diff --git a/packages/svelte-ux/src/lib/utils/dateRange.ts b/packages/svelte-ux/src/lib/utils/dateRange.ts index 624e537bb..4689e462d 100644 --- a/packages/svelte-ux/src/lib/utils/dateRange.ts +++ b/packages/svelte-ux/src/lib/utils/dateRange.ts @@ -1,6 +1,7 @@ import { startOfDay, isLeapYear, isAfter, isBefore, subYears } from 'date-fns'; -import { getDateFuncsByPeriodType, PeriodType } from './date'; +import { getDateFuncsByPeriodType } from './date'; +import { PeriodType } from './date_types'; export type DateRange = { from: Date | null; diff --git a/packages/svelte-ux/src/lib/utils/date_types.ts b/packages/svelte-ux/src/lib/utils/date_types.ts index 969362816..b0c6d1be5 100644 --- a/packages/svelte-ux/src/lib/utils/date_types.ts +++ b/packages/svelte-ux/src/lib/utils/date_types.ts @@ -1,3 +1,5 @@ +import type { DateRange } from './dateRange'; + export type SelectedDate = Date | Date[] | DateRange | null; export type Period = { @@ -110,15 +112,15 @@ export enum DateToken { } export type OrdinalSuffixes = { - one: string; - two: string; - few: string; - other: string; + one?: string; + two?: string; + few?: string; + other?: string; zero?: string; many?: string; }; export type DateFormatVariant = 'short' | 'default' | 'long'; -type DateFormatVariantPreset = { +export type DateFormatVariantPreset = { short?: CustomIntlDateTimeFormatOptions; default?: CustomIntlDateTimeFormatOptions; long?: CustomIntlDateTimeFormatOptions; diff --git a/packages/svelte-ux/src/lib/utils/format.ts b/packages/svelte-ux/src/lib/utils/format.ts index db4ffa270..97246eaa1 100644 --- a/packages/svelte-ux/src/lib/utils/format.ts +++ b/packages/svelte-ux/src/lib/utils/format.ts @@ -1,16 +1,8 @@ -import { - formatDate, - PeriodType, - type FormatDateOptions, - formatDateWithLocale, - getPeriodTypeName, - getPeriodTypeNameWithLocale, - getDayOfWeekName, - DayOfWeek, -} from './date'; +import { formatDateWithLocale, getPeriodTypeNameWithLocale, getDayOfWeekName } from './date'; import { formatNumberWithLocale } from './number'; import type { FormatNumberOptions, FormatNumberStyle } from './number'; import { defaultLocale, type LocaleSettings } from './locale'; +import { PeriodType, type FormatDateOptions, DayOfWeek } from './date_types'; export type FormatType = FormatNumberStyle | PeriodType; diff --git a/packages/svelte-ux/src/lib/utils/index.ts b/packages/svelte-ux/src/lib/utils/index.ts index f78febf69..ca6a767f1 100644 --- a/packages/svelte-ux/src/lib/utils/index.ts +++ b/packages/svelte-ux/src/lib/utils/index.ts @@ -1,5 +1,6 @@ // top-level exports -export { formatDate, PeriodType, DayOfWeek, DateToken } from './date'; +export { formatDate } from './date'; +export { PeriodType, DayOfWeek, DateToken } from './date_types'; export * from './duration'; export * from './file'; export { diff --git a/packages/svelte-ux/src/lib/utils/locale.ts b/packages/svelte-ux/src/lib/utils/locale.ts index e55f01dde..307a54cd7 100644 --- a/packages/svelte-ux/src/lib/utils/locale.ts +++ b/packages/svelte-ux/src/lib/utils/locale.ts @@ -9,6 +9,7 @@ import { } from './date_types'; import type { DictionaryMessages, DictionaryMessagesOptions } from './dictionary'; import type { FormatNumberOptions, FormatNumberStyle } from './number'; +import { getWeekStartsOnFromIntl } from './dateInternal'; function resolvedLocaleStore( forceLocales: Writable, @@ -183,6 +184,26 @@ export function createLocaleSettings( localeSettings: LocaleSettingsInput, base = defaultLocaleSettings ): LocaleSettings { + // if ordinalSuffixes is specified, we want to make sure that all are empty first + if (localeSettings.formats?.dates?.ordinalSuffixes) { + localeSettings.formats.dates.ordinalSuffixes = { + one: '', + two: '', + few: '', + other: '', + zero: '', + many: '', + ...localeSettings.formats.dates.ordinalSuffixes, + }; + } + + // if weekStartsOn is not specified, let's default to the local one + if (localeSettings.formats?.dates?.weekStartsOn === undefined) { + localeSettings = defaultsDeep(localeSettings, { + formats: { dates: { weekStartsOn: getWeekStartsOnFromIntl(localeSettings.locale) } }, + }); + } + return defaultsDeep(localeSettings, base); } diff --git a/packages/svelte-ux/src/routes/+layout.svelte b/packages/svelte-ux/src/routes/+layout.svelte index 74a7207b7..357c5bc84 100644 --- a/packages/svelte-ux/src/routes/+layout.svelte +++ b/packages/svelte-ux/src/routes/+layout.svelte @@ -19,9 +19,10 @@ import { settings } from '$lib/components/settings'; import type { PageData } from './$types'; - import { DateToken } from '$lib/utils/date'; import { lightThemes, darkThemes } from '$lib/styles/daisy'; + import { createLocaleSettings } from '$lib'; + import LanguageSelect from '$lib/components/LanguageSelect.svelte'; // import { lightThemes, darkThemes } from '$lib/styles/skeleton'; export let data: PageData; @@ -32,61 +33,32 @@ $: title = data.pr_id ? `🚧 (pr:${data.pr_id}) - ${baseTitle}` : baseTitle; settings({ - // Usefull to test different locales with the docs - // formats: { - // numbers: { - // defaults: { - // locales: 'fr', - // currency: 'EUR', - // }, - // }, - // dates: { - // locales: 'fr', - // weekStartsOn: 1, - // presets: { - // days: { - // long: { dateStyle: 'full' }, - // }, - // months: { - // default: [DateToken.Month_long], - // }, - // }, - // ordinalSuffixes: { - // fr: { - // one: 'er', - // two: '', - // few: '', - // other: '', - // }, - // }, - // }, - // dates: { - // locales: 'fr', - // weekStartsOn: 1, - // presets: { - // days: { - // long: { dateStyle: 'full' }, - // }, - // months: { - // default: [DateToken.Month_long], - // }, - // }, - // ordinalSuffixes: { - // fr: { - // one: 'er', - // two: '', - // few: '', - // other: '', - // }, - // }, - // }, - // }, - // dictionary: { - // Cancel: 'Annuler', - // Date: { - // Day: 'Jour', - // }, - // }, + // fallbackLocale: 'fr', + localeFormats: { + fr: createLocaleSettings({ + locale: 'fr', + formats: { + dates: { + baseParsing: 'dd/MM/yyyy', + ordinalSuffixes: { + one: 'er', + }, + }, + numbers: { + defaults: { + currency: 'EUR', + }, + }, + }, + dictionary: { + Cancel: 'Annuler', + Date: { + Day: 'Jour', + }, + }, + }), + }, + classes: { AppLayout: { aside: 'border-r', @@ -191,7 +163,8 @@ goto(e.detail.value)} /> -
+
+