diff --git a/apps/transloco-playground/src/app/locale/locale.component.html b/apps/transloco-playground/src/app/locale/locale.component.html index b14a3a0e..f796d918 100644 --- a/apps/transloco-playground/src/app/locale/locale.component.html +++ b/apps/transloco-playground/src/app/locale/locale.component.html @@ -7,82 +7,133 @@

Localization Support for Transloco

href="https://norbertlindenberg.com/2012/12/ecmascript-internationalization-api/index.html" target="_blank" >Internalization api. -

+ >. +

-
Select any locale from the list to see his format:
- +
Select any locale from the list to see his format:
+ -

Date Format

+

Date Format

- + -

Number Format

- +

Date Range Format

-

Currency Format

- + + +

Number Format

+ + +

Currency Format

+ diff --git a/apps/transloco-playground/src/app/locale/locale.component.ts b/apps/transloco-playground/src/app/locale/locale.component.ts index 8de8038f..691ff809 100644 --- a/apps/transloco-playground/src/app/locale/locale.component.ts +++ b/apps/transloco-playground/src/app/locale/locale.component.ts @@ -19,6 +19,7 @@ import { }) export default class LocaleComponent { date = new Date(2019, 7, 14, 0, 0, 0, 0); + endDate = new Date(2019, 8, 5, 0, 0, 0, 0); localeList: string[]; constructor( diff --git a/apps/transloco-playground/tsconfig.json b/apps/transloco-playground/tsconfig.json index d3e8c7d9..de276299 100644 --- a/apps/transloco-playground/tsconfig.json +++ b/apps/transloco-playground/tsconfig.json @@ -15,11 +15,11 @@ "strict": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "target": "es2020" + "target": "es2021" }, "angularCompilerOptions": { "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true } -} +} \ No newline at end of file diff --git a/libs/transloco-locale/src/index.ts b/libs/transloco-locale/src/index.ts index 031e6fc4..04c45fcb 100644 --- a/libs/transloco-locale/src/index.ts +++ b/libs/transloco-locale/src/index.ts @@ -9,8 +9,10 @@ export { export { TranslocoLocaleModule } from './lib/transloco-locale.module'; export { TRANSLOCO_DATE_TRANSFORMER, + TRANSLOCO_DATE_RANGE_TRANSFORMER, TRANSLOCO_NUMBER_TRANSFORMER, TranslocoDateTransformer, + TranslocoDateRangeTransformer, TranslocoNumberTransformer, DefaultDateTransformer, DefaultNumberTransformer, @@ -19,6 +21,7 @@ export { provideTranslocoLocaleLangMapping, provideTranslocoLocaleCurrencyMapping, provideTranslocoDateTransformer, + provideTranslocoDateRangeTransformer, provideTranslocoDefaultCurrency, provideTranslocoLocale, provideTranslocoNumberTransformer, @@ -29,6 +32,7 @@ export * from './lib/transloco-locale.types'; export { TranslocoCurrencyPipe, TranslocoDatePipe, + TranslocoDateRangePipe, TranslocoDecimalPipe, TranslocoPercentPipe, BaseLocalePipe, diff --git a/libs/transloco-locale/src/lib/helpers.ts b/libs/transloco-locale/src/lib/helpers.ts index 2f545ec7..bba85aa9 100644 --- a/libs/transloco-locale/src/lib/helpers.ts +++ b/libs/transloco-locale/src/lib/helpers.ts @@ -43,6 +43,21 @@ export function localizeDate( return ''; } +export function localizeDateRange( + startDate: Date, + endDate: Date, + locale: Locale, + options: DateFormatOptions, +): string { + if (isDate(startDate) && isDate(endDate)) { + return new Intl.DateTimeFormat(locale, options as any).formatRange( + startDate, + endDate, + ); + } + return ''; +} + export function isDate(value: any): boolean { return value instanceof Date && !isNaN(value); } diff --git a/libs/transloco-locale/src/lib/pipes/index.ts b/libs/transloco-locale/src/lib/pipes/index.ts index aa069521..aa0c1ae6 100644 --- a/libs/transloco-locale/src/lib/pipes/index.ts +++ b/libs/transloco-locale/src/lib/pipes/index.ts @@ -1,5 +1,6 @@ export { TranslocoCurrencyPipe } from './transloco-currency.pipe'; export { TranslocoDatePipe } from './transloco-date.pipe'; +export { TranslocoDateRangePipe } from './transloco-date-range.pipe'; export { TranslocoDecimalPipe } from './transloco-decimal.pipe'; export { BaseLocalePipe } from './base-locale.pipe'; export { TranslocoPercentPipe } from './transloco-percent.pipe'; diff --git a/libs/transloco-locale/src/lib/pipes/transloco-date-range.pipe.ts b/libs/transloco-locale/src/lib/pipes/transloco-date-range.pipe.ts new file mode 100644 index 00000000..f2fcfab8 --- /dev/null +++ b/libs/transloco-locale/src/lib/pipes/transloco-date-range.pipe.ts @@ -0,0 +1,57 @@ +import { inject, Pipe, PipeTransform } from '@angular/core'; +import { isNil } from '@jsverse/transloco'; + +import { getDefaultOptions } from '../shared'; +import { TRANSLOCO_LOCALE_CONFIG } from '../transloco-locale.config'; +import { + Locale, + DateFormatOptions, + LocaleConfig, + ValidDate, +} from '../transloco-locale.types'; + +import { BaseLocalePipe } from './base-locale.pipe'; +import { TranslocoDatePipe } from './transloco-date.pipe'; + +@Pipe({ + name: 'translocoDateRange', + pure: false, + standalone: true, +}) +export class TranslocoDateRangePipe + extends BaseLocalePipe + implements PipeTransform +{ + private localeConfig: LocaleConfig = inject(TRANSLOCO_LOCALE_CONFIG); + + /** + * Transform two dates into the locale's date range format. + * + * The date expression: a `Date` object, a number + * (milliseconds since UTC epoch), or an ISO string (https://www.w3.org/TR/NOTE-datetime). + * + * @example + * + * startDate | translocoDateRange: endDate : {} : en-US // 9/10–10/10/2019 + * startDate | translocoDate: endDate : { dateStyle: 'medium', timeStyle: 'medium' } : en-US // Sep 10, 2019, 10:46:12 PM – Oct 10, 2019, 10:46:12 PM + * '2019-02-08' | translocoDate: '2020-03-10' : { dateStyle: 'medium' } // Feb 8 2019 – Mar 10 2020 + */ + transform( + startDate: ValidDate, + endDate: ValidDate, + options: DateFormatOptions = {}, + locale?: Locale, + ) { + if (isNil(startDate)) return ''; + if (isNil(endDate)) { + return inject(TranslocoDatePipe).transform(startDate, options, locale); + } + + locale = this.getLocale(locale); + + return this.localeService.localizeDateRange(startDate, endDate, locale, { + ...getDefaultOptions(locale, 'date', this.localeConfig), + ...options, + }); + } +} diff --git a/libs/transloco-locale/src/lib/tests/pipes/transloco-date-range.pipe.spec.ts b/libs/transloco-locale/src/lib/tests/pipes/transloco-date-range.pipe.spec.ts new file mode 100644 index 00000000..527b74bf --- /dev/null +++ b/libs/transloco-locale/src/lib/tests/pipes/transloco-date-range.pipe.spec.ts @@ -0,0 +1,140 @@ +import { SpectatorPipe } from '@ngneat/spectator'; + +import { TranslocoDateRangePipe } from '../../pipes'; +import { + LOCALE_CONFIG_MOCK, + provideTranslocoLocaleConfigMock, + provideTranslocoServiceMock, +} from '../mocks'; +import { createLocalePipeFactory } from '../utils'; + +describe('TranslocoDateRangePipe', () => { + let intlSpy: jasmine.Spy<(typeof Intl)['DateTimeFormat']>; + let spectator: SpectatorPipe; + const pipeFactory = createLocalePipeFactory(TranslocoDateRangePipe); + + const startDate = new Date(2019, 9, 7, 12, 0, 0); + const endDate = new Date(2020, 4, 15, 16, 0, 0); + + function getIntlCallArgs() { + const [locale, options] = intlSpy.calls.argsFor(0); + + return [locale!, options!] as const; + } + + beforeEach(() => { + intlSpy = spyOn(Intl, 'DateTimeFormat').and.callThrough(); + }); + + it('should transform date to locale formatted date', () => { + spectator = pipeFactory(`{{ startDate | translocoDateRange:endDate }}`, { + hostProps: { + startDate: startDate, + endDate: endDate, + }, + }); + expect(spectator.element).toHaveText('9/7/2019 – 4/15/2020'); + }); + + it('should consider a given format over the current locale', () => { + spectator = pipeFactory(`{{ startDate | translocoDateRange:endDate:config }}`, { + hostProps: { + startDate: startDate, + config: { dateStyle: 'medium', timeStyle: 'medium' }, + }, + }); + const [, { dateStyle, timeStyle }] = getIntlCallArgs(); + expect(dateStyle).toEqual('medium'); + expect(timeStyle).toEqual('medium'); + }); + + it('should consider a global date config', () => { + spectator = pipeFactory(`{{ startDate | translocoDateRange:endDate }}`, { + hostProps: { + startDate: startDate, + }, + providers: [provideTranslocoLocaleConfigMock(LOCALE_CONFIG_MOCK)], + }); + const [, { dateStyle, timeStyle }] = getIntlCallArgs(); + expect(dateStyle).toEqual('medium'); + expect(timeStyle).toEqual('medium'); + }); + + it('should consider a locale config over global', () => { + spectator = pipeFactory(`{{ startDate | translocoDateRange:endDate }}`, { + hostProps: { + startDate: startDate, + }, + providers: [ + provideTranslocoLocaleConfigMock(LOCALE_CONFIG_MOCK), + provideTranslocoServiceMock('es-ES'), + ], + }); + const [locale, { dateStyle, timeStyle }] = getIntlCallArgs(); + expect(locale).toEqual('es-ES'); + expect(dateStyle).toEqual('long'); + expect(timeStyle).toEqual('long'); + }); + + it('should consider a given config over the global config', () => { + spectator = pipeFactory(`{{ startDate | translocoDateRange:endDate:config }}`, { + hostProps: { + startDate: startDate, + config: { dateStyle: 'full' }, + }, + providers: [provideTranslocoLocaleConfigMock(LOCALE_CONFIG_MOCK)], + }); + const [, { dateStyle, timeStyle }] = getIntlCallArgs(); + expect(dateStyle).toEqual('full'); + expect(timeStyle).toEqual('medium'); + }); + + describe('None date values', () => { + it('should handle null', () => { + spectator = pipeFactory(`{{ null | translocoDateRange:null }}`); + expect(spectator.element).toHaveText(''); + }); + it('should handle {}', () => { + spectator = pipeFactory(`{{ {} | translocoDateRange:{} }}`); + expect(spectator.element).toHaveText(''); + }); + it('should handle none number string', () => { + spectator = pipeFactory(`{{ 'none number string' | translocoDateRange:'none number string' }}`); + expect(spectator.element).toHaveText(''); + }); + }); + + it('should transform number to date', () => { + spectator = pipeFactory(`{{ startDate | translocoDateRange:endDate:config }}`, { + hostProps: { + startDate: 0, + endDate: 1000000000, + config: { timeZone: 'UTC' }, + }, + }); + expect(spectator.element).toHaveText('1/1/1970 – 9/9/2001'); + }); + + it('should transform string to date', () => { + spectator = pipeFactory(`{{ startDate | translocoDateRange:config }}`, { + hostProps: { + startDate: '2019-02-08', + endDate: '2020-05-15', + }, + }); + expect(spectator.element).toHaveText('2/8/2019–5/15/2020'); + }); + + it('should transform an ISO 8601 string to date', () => { + spectator = pipeFactory(`{{ startDate | translocoDateRange:config:locale }}`, { + hostProps: { + startDate: '2019-09-12T19:51:33Z', + endDate: '2019-10-12T19:51:33Z', + config: { timeZone: 'UTC' }, + locale: 'en-US', + }, + providers: [provideTranslocoLocaleConfigMock(LOCALE_CONFIG_MOCK)], + }); + expect(spectator.element).toHaveText('Sep 12, 2019, 7:51:33 PM – Oct 12, 2019, 7:51:33 PM'); + }); +}); diff --git a/libs/transloco-locale/src/lib/transloco-locale.module.ts b/libs/transloco-locale/src/lib/transloco-locale.module.ts index 2a0891c5..a62275c5 100644 --- a/libs/transloco-locale/src/lib/transloco-locale.module.ts +++ b/libs/transloco-locale/src/lib/transloco-locale.module.ts @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core'; import { TranslocoCurrencyPipe, TranslocoDatePipe, + TranslocoDateRangePipe, TranslocoDecimalPipe, TranslocoPercentPipe, } from './pipes'; @@ -10,6 +11,7 @@ import { const decl = [ TranslocoCurrencyPipe, TranslocoDatePipe, + TranslocoDateRangePipe, TranslocoDecimalPipe, TranslocoPercentPipe, ]; @@ -18,4 +20,4 @@ const decl = [ imports: decl, exports: decl, }) -export class TranslocoLocaleModule {} +export class TranslocoLocaleModule { } diff --git a/libs/transloco-locale/src/lib/transloco-locale.providers.ts b/libs/transloco-locale/src/lib/transloco-locale.providers.ts index b9229d25..9c2a04fb 100644 --- a/libs/transloco-locale/src/lib/transloco-locale.providers.ts +++ b/libs/transloco-locale/src/lib/transloco-locale.providers.ts @@ -18,11 +18,14 @@ import { } from './transloco-locale.config'; import { DefaultDateTransformer, + DefaultDateRangeTransformer, DefaultNumberTransformer, TRANSLOCO_DATE_TRANSFORMER, + TRANSLOCO_DATE_RANGE_TRANSFORMER, TRANSLOCO_NUMBER_TRANSFORMER, TranslocoDateTransformer, TranslocoNumberTransformer, + TranslocoDateRangeTransformer, } from './transloco-locale.transformers'; export function provideTranslocoLocale(config?: TranslocoLocaleConfig) { @@ -38,6 +41,7 @@ export function provideTranslocoLocale(config?: TranslocoLocaleConfig) { provideTranslocoLocaleLangMapping(merged.langToLocaleMapping), provideTranslocoLocaleCurrencyMapping(merged.localeToCurrencyMapping), provideTranslocoDateTransformer(DefaultDateTransformer), + provideTranslocoDateRangeTransformer(DefaultDateRangeTransformer), provideTranslocoNumberTransformer(DefaultNumberTransformer), ]; } @@ -101,6 +105,16 @@ export function provideTranslocoDateTransformer( }, ]); } +export function provideTranslocoDateRangeTransformer( + transformer: Type +) { + return makeEnvironmentProviders([ + { + provide: TRANSLOCO_DATE_RANGE_TRANSFORMER, + useClass: transformer, + }, + ]); +} export function provideTranslocoNumberTransformer( transformer: Type diff --git a/libs/transloco-locale/src/lib/transloco-locale.service.ts b/libs/transloco-locale/src/lib/transloco-locale.service.ts index f71a8fad..20df1575 100644 --- a/libs/transloco-locale/src/lib/transloco-locale.service.ts +++ b/libs/transloco-locale/src/lib/transloco-locale.service.ts @@ -13,6 +13,7 @@ import { TRANSLOCO_LOCALE_LANG_MAPPING, } from './transloco-locale.config'; import { + TRANSLOCO_DATE_RANGE_TRANSFORMER, TRANSLOCO_DATE_TRANSFORMER, TRANSLOCO_NUMBER_TRANSFORMER, } from './transloco-locale.transformers'; @@ -35,6 +36,7 @@ export class TranslocoLocaleService implements OnDestroy { private localeCurrencyMapping = inject(TRANSLOCO_LOCALE_CURRENCY_MAPPING); private numberTransformer = inject(TRANSLOCO_NUMBER_TRANSFORMER); private dateTransformer = inject(TRANSLOCO_DATE_TRANSFORMER); + private dateRangeTransformer = inject(TRANSLOCO_DATE_RANGE_TRANSFORMER); private localeConfig: LocaleConfig = inject(TRANSLOCO_LOCALE_CONFIG); private _locale = @@ -110,6 +112,31 @@ export class TranslocoLocaleService implements OnDestroy { return this.dateTransformer.transform(toDate(date), locale, resolved); } + /** + /** + * Transform two dates into the locale's date range format. + * + * The date expression: a `Date` object, a number + * (milliseconds since UTC epoch), or an ISO string (https://www.w3.org/TR/NOTE-datetime). + * + * @example + * + * startDate | translocoDateRange: endDate : {} : en-US // 9/10–10/10/2019 + * startDate | translocoDate: endDate : { dateStyle: 'medium', timeStyle: 'medium' } : en-US // Sep 10, 2019, 10:46:12 PM – Oct 10, 2019, 10:46:12 PM + * '2019-02-08' | translocoDate: '2020-03-10' : { dateStyle: 'medium' } // Feb 8 2019 – Mar 10 2020 + */ + localizeDateRange( + startDate: ValidDate, + endDate: ValidDate, + locale: Locale = this.getLocale(), + options: DateFormatOptions = {} + ): string { + const resolved = + options ?? getDefaultOptions(locale, 'date', this.localeConfig); + + return this.dateRangeTransformer.transform(toDate(startDate), toDate(endDate), locale, resolved); + } + /** * Transform a number into the locale's number format according to the number type. * diff --git a/libs/transloco-locale/src/lib/transloco-locale.transformers.ts b/libs/transloco-locale/src/lib/transloco-locale.transformers.ts index fb912420..879ff6fc 100644 --- a/libs/transloco-locale/src/lib/transloco-locale.transformers.ts +++ b/libs/transloco-locale/src/lib/transloco-locale.transformers.ts @@ -1,6 +1,6 @@ import { InjectionToken } from '@angular/core'; -import { localizeNumber, localizeDate } from './helpers'; +import { localizeNumber, localizeDate, localizeDateRange, } from './helpers'; import { Locale, DateFormatOptions, @@ -10,6 +10,9 @@ import { export interface TranslocoDateTransformer { transform(date: Date, locale: Locale, options: DateFormatOptions): string; } +export interface TranslocoDateRangeTransformer { + transform(startDate: Date, endDate: Date, locale: Locale, options: DateFormatOptions): string; +} export interface TranslocoNumberTransformer { transform( value: number | string, @@ -21,6 +24,8 @@ export interface TranslocoNumberTransformer { export const TRANSLOCO_DATE_TRANSFORMER = new InjectionToken('TRANSLOCO_DATE_TRANSFORMER'); +export const TRANSLOCO_DATE_RANGE_TRANSFORMER = + new InjectionToken('TRANSLOCO_DATE_TRANSFORMER'); export const TRANSLOCO_NUMBER_TRANSFORMER = new InjectionToken( 'TRANSLOCO_NUMBER_TRANSFORMER' @@ -35,6 +40,16 @@ export class DefaultDateTransformer implements TranslocoDateTransformer { return localizeDate(date, locale, options); } } +export class DefaultDateRangeTransformer implements TranslocoDateRangeTransformer { + public transform( + startDate: Date, + endDate: Date, + locale: Locale, + options: DateFormatOptions + ): string { + return localizeDateRange(startDate, endDate, locale, options); + } +} export class DefaultNumberTransformer implements TranslocoNumberTransformer { public transform( value: number | string, diff --git a/libs/transloco-locale/tsconfig.lib.json b/libs/transloco-locale/tsconfig.lib.json index 674998f0..f8b8bc07 100644 --- a/libs/transloco-locale/tsconfig.lib.json +++ b/libs/transloco-locale/tsconfig.lib.json @@ -7,8 +7,17 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "ES2020"] + "lib": [ + "dom", + "ES2020", + "ES2021.Intl" + ] }, - "exclude": ["src/test-setup.ts", "**/*.spec.ts"], - "include": ["**/*.ts"] -} + "exclude": [ + "src/test-setup.ts", + "**/*.spec.ts" + ], + "include": [ + "**/*.ts" + ] +} \ No newline at end of file diff --git a/libs/transloco-persist-translations/tsconfig.lib.json b/libs/transloco-persist-translations/tsconfig.lib.json index 18b202a8..ebaedcdc 100644 --- a/libs/transloco-persist-translations/tsconfig.lib.json +++ b/libs/transloco-persist-translations/tsconfig.lib.json @@ -7,8 +7,18 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2020"] + "lib": [ + "dom", + "es2020", + "ES2021.Intl" + ] }, - "exclude": ["src/test-setup.ts", "**/*.spec.ts", "src/lib/tests/mocks.ts"], - "include": ["**/*.ts"] -} + "exclude": [ + "src/test-setup.ts", + "**/*.spec.ts", + "src/lib/tests/mocks.ts" + ], + "include": [ + "**/*.ts" + ] +} \ No newline at end of file diff --git a/libs/transloco-preload-langs/tsconfig.lib.json b/libs/transloco-preload-langs/tsconfig.lib.json index 82243b44..4c820dba 100644 --- a/libs/transloco-preload-langs/tsconfig.lib.json +++ b/libs/transloco-preload-langs/tsconfig.lib.json @@ -7,8 +7,17 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2020"] + "lib": [ + "dom", + "es2020", + "ES2021.Intl" + ] }, - "exclude": ["src/test-setup.ts", "**/*.spec.ts"], - "include": ["**/*.ts"] -} + "exclude": [ + "src/test-setup.ts", + "**/*.spec.ts" + ], + "include": [ + "**/*.ts" + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 129126fd..6de9188b 100644 --- a/package.json +++ b/package.json @@ -110,3 +110,4 @@ "includedScripts": [] } } + diff --git a/tsconfig.base.json b/tsconfig.base.json index 6f6d8c96..000d827f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -10,17 +10,27 @@ "importHelpers": true, "target": "ES2022", "module": "esnext", - "lib": ["es2020", "dom"], + "lib": [ + "es2020", + "dom", + "ES2021.Intl" + ], "skipLibCheck": true, "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { - "@jsverse/transloco": ["libs/transloco/src/index.ts"], - "@jsverse/transloco-locale": ["libs/transloco-locale/src/index.ts"], + "@jsverse/transloco": [ + "libs/transloco/src/index.ts" + ], + "@jsverse/transloco-locale": [ + "libs/transloco-locale/src/index.ts" + ], "@jsverse/transloco-messageformat": [ "libs/transloco-messageformat/src/index.ts" ], - "@jsverse/transloco-optimize": ["libs/transloco-optimize/src/index.ts"], + "@jsverse/transloco-optimize": [ + "libs/transloco-optimize/src/index.ts" + ], "@jsverse/transloco-persist-lang": [ "libs/transloco-persist-lang/src/index.ts" ], @@ -36,9 +46,16 @@ "@jsverse/transloco-scoped-libs": [ "libs/transloco-scoped-libs/src/index.ts" ], - "@jsverse/transloco-utils": ["libs/transloco-utils/src/index.ts"], - "@jsverse/transloco-validator": ["libs/transloco-validator/src/index.ts"] + "@jsverse/transloco-utils": [ + "libs/transloco-utils/src/index.ts" + ], + "@jsverse/transloco-validator": [ + "libs/transloco-validator/src/index.ts" + ] } }, - "exclude": ["node_modules", "tmp"] -} + "exclude": [ + "node_modules", + "tmp" + ] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 540f38eb..a8daf592 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,8 +13,14 @@ "experimentalDecorators": true, "importHelpers": true, "target": "ES2022", - "typeRoots": ["node_modules/@types"], - "lib": ["es2018", "dom"], + "typeRoots": [ + "node_modules/@types" + ], + "lib": [ + "es2018", + "dom", + "ES2021.Intl" + ], "useDefineForClassFields": false } -} +} \ No newline at end of file