mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-03-16 03:32:18 +00:00
feat: date-fns
This commit is contained in:
@@ -101,7 +101,7 @@ export function initializerFactory(
|
|||||||
// this language will be used as a fallback when a translation isn't found in the current language
|
// this language will be used as a fallback when a translation isn't found in the current language
|
||||||
translateService.setDefaultLang('en');
|
translateService.setDefaultLang('en');
|
||||||
translateService.use(languageCode);
|
translateService.use(languageCode);
|
||||||
const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode);
|
const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode, translateService);
|
||||||
setDefaultOptions({locale: dateFnsLocale});
|
setDefaultOptions({locale: dateFnsLocale});
|
||||||
dateFnsConfigurationService.setLocale(dateFnsLocale);
|
dateFnsConfigurationService.setLocale(dateFnsLocale);
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
>
|
>
|
||||||
<ion-list-header>
|
<ion-list-header>
|
||||||
{{ frequency.children[0].item.repeatFrequency ? (frequency.children[0].item.repeatFrequency |
|
{{ frequency.children[0].item.repeatFrequency ? (frequency.children[0].item.repeatFrequency |
|
||||||
dfnsParseDuration | dfnsFormatFrequency | sentencecase) : ('data.chips.add_events.popover.SINGLE' |
|
dfnsParseDuration | dfnsFormatFrequencyPure | sentencecase) :
|
||||||
translate | titlecase) }}
|
('data.chips.add_events.popover.SINGLE' | translate | titlecase) }}
|
||||||
</ion-list-header>
|
</ion-list-header>
|
||||||
</ion-checkbox>
|
</ion-checkbox>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ import {
|
|||||||
ParseIsoPipeModule,
|
ParseIsoPipeModule,
|
||||||
} from 'ngx-date-fns';
|
} from 'ngx-date-fns';
|
||||||
import {ParseDurationPipe} from '../../translation/date-time/parse-duration.pipe';
|
import {ParseDurationPipe} from '../../translation/date-time/parse-duration.pipe';
|
||||||
import {DfnsFormatFrequencyPipe} from '../../translation/date-time/format-frequency.pipe';
|
import {DfnsFormatFrequencyPurePipe} from '../../translation/date-time/format-frequency.pipe';
|
||||||
import {FormatRelativeDatePipe} from '../../translation/date-time/format-relative-date.pipe';
|
import {DfnsFormatRelativeDatePurePipe} from '../../translation/date-time/format-relative-date.pipe';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module for handling data
|
* Module for handling data
|
||||||
@@ -207,9 +207,9 @@ import {FormatRelativeDatePipe} from '../../translation/date-time/format-relativ
|
|||||||
ParseDurationPipe,
|
ParseDurationPipe,
|
||||||
FormatDurationPipeModule,
|
FormatDurationPipeModule,
|
||||||
FormatRelativeToNowPipeModule,
|
FormatRelativeToNowPipeModule,
|
||||||
DfnsFormatFrequencyPipe,
|
DfnsFormatFrequencyPurePipe,
|
||||||
FormatDistanceToNowPipeModule,
|
FormatDistanceToNowPipeModule,
|
||||||
FormatRelativeDatePipe,
|
DfnsFormatRelativeDatePurePipe,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CoordinatedSearchProvider,
|
CoordinatedSearchProvider,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {ChangeDetectionStrategy, Component} from '@angular/core';
|
|||||||
import {MapPosition, PositionService} from '../../map/position.service';
|
import {MapPosition, PositionService} from '../../map/position.service';
|
||||||
import {Geolocation} from '@capacitor/geolocation';
|
import {Geolocation} from '@capacitor/geolocation';
|
||||||
import {BehaviorSubject, from} from 'rxjs';
|
import {BehaviorSubject, from} from 'rxjs';
|
||||||
import {pauseWhen} from '../../../util/pause-when';
|
import {pauseWhen} from '../../../util/rxjs/pause-when';
|
||||||
import {map, retry, startWith, take} from 'rxjs/operators';
|
import {map, retry, startWith, take} from 'rxjs/operators';
|
||||||
import {SCSearchFilter, SCSearchSort} from '@openstapps/core';
|
import {SCSearchFilter, SCSearchSort} from '@openstapps/core';
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
<ion-segment [(ngModel)]="selectedDay" mode="md">
|
<ion-segment [(ngModel)]="selectedDay" mode="md">
|
||||||
<ion-segment-button *ngFor="let day of dishes | keyvalue" [value]="day.key">
|
<ion-segment-button *ngFor="let day of dishes | keyvalue" [value]="day.key">
|
||||||
<ion-label class="ion-hide-sm-down"
|
<ion-label class="ion-hide-sm-down"
|
||||||
>{{ day.key | dfnsParseIso | dfnsFormatRelativeDate | sentencecase }}</ion-label
|
>{{ day.key | dfnsParseIso | dfnsFormatRelativeDatePure | sentencecase }}</ion-label
|
||||||
>
|
>
|
||||||
<ion-label class="ion-hide-sm-up"
|
<ion-label class="ion-hide-sm-up"
|
||||||
>{{ day.key | dfnsParseIso | dfnsFormatRelativeDate | sentencecase }}</ion-label
|
>{{ day.key | dfnsParseIso | dfnsFormatRelativeDatePure | sentencecase }}</ion-label
|
||||||
>
|
>
|
||||||
</ion-segment-button>
|
</ion-segment-button>
|
||||||
</ion-segment>
|
</ion-segment>
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
<ion-item slot="header">
|
<ion-item slot="header">
|
||||||
<!-- TODO: when using date-fns, use https://date-fns.org/v2.30.0/docs/formatRelative -->
|
<!-- TODO: when using date-fns, use https://date-fns.org/v2.30.0/docs/formatRelative -->
|
||||||
<ion-label
|
<ion-label
|
||||||
>{{ myCoursesDay[0] | dfnsParseIso | dfnsFormatRelativeDate | titlecase }} - {{ ('profile.courses.' +
|
>{{ myCoursesDay[0] | dfnsParseIso | dfnsFormatRelativeDatePure | titlecase }} - {{
|
||||||
(myCoursesDay[1].length === 0 ? 'NO' : myCoursesDay[1].length === 1 ? 'ONE' : 'MANY' ) + '_EVENT') |
|
('profile.courses.' + (myCoursesDay[1].length === 0 ? 'NO' : myCoursesDay[1].length === 1 ? 'ONE' :
|
||||||
translate: {count: myCoursesDay[1].length} }}</ion-label
|
'MANY' ) + '_EVENT') | translate: {count: myCoursesDay[1].length} }}</ion-label
|
||||||
>
|
>
|
||||||
<ion-icon class="ion-accordion-toggle-icon" name="expand_more"></ion-icon>
|
<ion-icon class="ion-accordion-toggle-icon" name="expand_more"></ion-icon>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
|||||||
import {DataModule} from '../data/data.module';
|
import {DataModule} from '../data/data.module';
|
||||||
import {MyCoursesComponent} from './page/my-courses.component';
|
import {MyCoursesComponent} from './page/my-courses.component';
|
||||||
import {FormatPipeModule, ParseIsoPipeModule} from 'ngx-date-fns';
|
import {FormatPipeModule, ParseIsoPipeModule} from 'ngx-date-fns';
|
||||||
import {FormatRelativeDatePipe} from '../../translation/date-time/format-relative-date.pipe';
|
import {DfnsFormatRelativeDatePurePipe} from '../../translation/date-time/format-relative-date.pipe';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -50,7 +50,7 @@ const routes: Routes = [
|
|||||||
ThingTranslateModule,
|
ThingTranslateModule,
|
||||||
DataModule,
|
DataModule,
|
||||||
FormatPipeModule,
|
FormatPipeModule,
|
||||||
FormatRelativeDatePipe,
|
DfnsFormatRelativeDatePurePipe,
|
||||||
ParseIsoPipeModule,
|
ParseIsoPipeModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -96,79 +96,6 @@ export class StringSplitPipe implements PipeTransform {
|
|||||||
return this.value as never;
|
return this.value as never;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'durationLocalized',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class DurationLocalizedPipe implements PipeTransform, OnDestroy {
|
|
||||||
locale: string;
|
|
||||||
|
|
||||||
onLangChange?: Subscription;
|
|
||||||
|
|
||||||
value: string;
|
|
||||||
|
|
||||||
frequencyPrefixes: {[iso6391Code: string]: string} = {
|
|
||||||
de: 'alle',
|
|
||||||
en: 'every',
|
|
||||||
es: 'cada',
|
|
||||||
pt: 'a cada',
|
|
||||||
fr: 'tous les',
|
|
||||||
cn: '每',
|
|
||||||
ru: 'kаждые',
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(private readonly translate: TranslateService) {
|
|
||||||
this.locale = translate.currentLang;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _dispose(): void {
|
|
||||||
if (this.onLangChange?.closed === false) {
|
|
||||||
this.onLangChange?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this._dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param value An ISO 8601 duration string
|
|
||||||
* @param isFrequency Boolean indicating if this duration is to be interpreted as repeat frequency
|
|
||||||
*/
|
|
||||||
transform(value: string | unknown, isFrequency = false): string {
|
|
||||||
this.updateValue(value, isFrequency);
|
|
||||||
this._dispose();
|
|
||||||
if (this.onLangChange?.closed === true) {
|
|
||||||
this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.locale = event.lang;
|
|
||||||
this.updateValue(value, isFrequency);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateValue(value: string | unknown, isFrequency = false): void {
|
|
||||||
if (typeof value !== 'string') {
|
|
||||||
logger.warn(`durationLocalized pipe unable to parse input: ${value}`);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFrequency) {
|
|
||||||
const fequencyPrefix = Object.keys(this.frequencyPrefixes).filter(element =>
|
|
||||||
this.locale.includes(element),
|
|
||||||
);
|
|
||||||
this.value = [
|
|
||||||
fequencyPrefix.length > 0 ? this.frequencyPrefixes[fequencyPrefix[0]] : this.frequencyPrefixes.en,
|
|
||||||
moment.duration(value).humanize(),
|
|
||||||
].join(' ');
|
|
||||||
} else {
|
|
||||||
this.value = moment.duration(value).humanize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@Pipe({
|
@Pipe({
|
||||||
|
|||||||
@@ -1,68 +1,14 @@
|
|||||||
import {Injectable, OnDestroy, Pipe, PipeTransform} from '@angular/core';
|
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||||
import {Subscription} from 'rxjs';
|
import {formatFrequency} from './format-frequency';
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
|
||||||
import {formatDuration} from 'date-fns';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'dfnsFormatFrequency',
|
name: 'dfnsFormatFrequencyPure',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
pure: false,
|
pure: true,
|
||||||
})
|
})
|
||||||
export class DfnsFormatFrequencyPipe implements PipeTransform, OnDestroy {
|
export class DfnsFormatFrequencyPurePipe implements PipeTransform {
|
||||||
locale: string;
|
transform(duration: Duration): string {
|
||||||
|
return formatFrequency(duration);
|
||||||
onLangChange?: Subscription;
|
|
||||||
|
|
||||||
value: string;
|
|
||||||
|
|
||||||
frequencyPrefixes: {[iso6391Code: string]: string} = {
|
|
||||||
de: 'alle',
|
|
||||||
en: 'every',
|
|
||||||
es: 'cada',
|
|
||||||
pt: 'a cada',
|
|
||||||
fr: 'tous les',
|
|
||||||
cn: '每',
|
|
||||||
ru: 'kаждые',
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(private readonly translate: TranslateService) {
|
|
||||||
this.locale = translate.currentLang;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _dispose(): void {
|
|
||||||
if (this.onLangChange?.closed === false) {
|
|
||||||
this.onLangChange?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this._dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param value An ISO 8601 duration string
|
|
||||||
*/
|
|
||||||
transform(value: Duration): string {
|
|
||||||
this.updateValue(value);
|
|
||||||
this._dispose();
|
|
||||||
if (this.onLangChange?.closed === true) {
|
|
||||||
this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.locale = event.lang;
|
|
||||||
this.updateValue(value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateValue(value: Duration): void {
|
|
||||||
const fequencyPrefix = Object.keys(this.frequencyPrefixes).filter(element =>
|
|
||||||
this.locale.includes(element),
|
|
||||||
);
|
|
||||||
this.value = [
|
|
||||||
fequencyPrefix.length > 0 ? this.frequencyPrefixes[fequencyPrefix[0]] : this.frequencyPrefixes.en,
|
|
||||||
formatDuration(value),
|
|
||||||
].join(' ');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import {Duration, formatDuration, getDefaultOptions} from 'date-fns';
|
||||||
|
import {LocaleExtension} from '../dfns-locale';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a duration to a frequency
|
||||||
|
* @param duration the duration to format as a frequency
|
||||||
|
* @example "alle 3 Tage"
|
||||||
|
*/
|
||||||
|
export function formatFrequency(duration: Duration): string {
|
||||||
|
const {locale} = getDefaultOptions() as {locale: LocaleExtension};
|
||||||
|
locale.formatFrequencyOptions;
|
||||||
|
|
||||||
|
// This will break...
|
||||||
|
const formatted = formatDuration(duration);
|
||||||
|
const plural = new Intl.PluralRules(locale.code).select(Number(/\d+/.exec(formatted)?.[0]));
|
||||||
|
const formatString = locale.formatFrequencyOptions[plural] ?? locale.formatFrequencyOptions['many']!;
|
||||||
|
|
||||||
|
return formatString
|
||||||
|
.replaceAll(/(['"](?=\w))|((?<=\w)['"])/g, '')
|
||||||
|
.replaceAll(/{{\s*duration\s*}}/g, formatted)
|
||||||
|
.replaceAll(/{{\s*suffix\s*}}/g, formatted.replace(/^\d+\s*/, ''));
|
||||||
|
}
|
||||||
@@ -1,67 +1,13 @@
|
|||||||
import {Pipe, PipeTransform} from '@angular/core';
|
import {Pipe, PipeTransform} from '@angular/core';
|
||||||
import {DateFnsConfigurationService} from 'ngx-date-fns';
|
import {formatRelativeDate} from './format-relative-date';
|
||||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
|
||||||
import {formatRelative} from 'date-fns';
|
|
||||||
|
|
||||||
const formatRelativeLocaleEn = {
|
|
||||||
lastWeek: "'last' eeee",
|
|
||||||
yesterday: "'yesterday'",
|
|
||||||
today: "'today'",
|
|
||||||
tomorrow: "'tomorrow'",
|
|
||||||
nextWeek: "'next' eeee",
|
|
||||||
other: 'pp',
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatRelativeLocaleDe = {
|
|
||||||
lastWeek: "'letzten' eeee",
|
|
||||||
yesterday: "'gestern'",
|
|
||||||
today: "'heute'",
|
|
||||||
tomorrow: "'morgen'",
|
|
||||||
nextWeek: 'eeee',
|
|
||||||
other: 'pp',
|
|
||||||
};
|
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'dfnsFormatRelativeDate',
|
name: 'dfnsFormatRelativeDatePure',
|
||||||
pure: false,
|
pure: true,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
})
|
})
|
||||||
export class FormatRelativeDatePipe implements PipeTransform {
|
export class DfnsFormatRelativeDatePurePipe implements PipeTransform {
|
||||||
locale: Locale;
|
transform(date: Date | number, baseDate: Date | number = Date.now()): string {
|
||||||
|
return formatRelativeDate(date, baseDate);
|
||||||
value: string;
|
|
||||||
|
|
||||||
input: Date | number;
|
|
||||||
|
|
||||||
base: Date | number;
|
|
||||||
|
|
||||||
constructor(private dfnsConfig: DateFnsConfigurationService) {
|
|
||||||
this.updateLocale();
|
|
||||||
dfnsConfig.localeChanged.pipe(takeUntilDestroyed()).subscribe(this.updateLocale.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateLocale() {
|
|
||||||
this.locale = {...this.dfnsConfig.locale()!};
|
|
||||||
const format = {
|
|
||||||
['de']: formatRelativeLocaleDe,
|
|
||||||
['en-US']: formatRelativeLocaleEn,
|
|
||||||
}[this.locale.code!];
|
|
||||||
this.locale.formatRelative = function (token) {
|
|
||||||
return format![token as keyof typeof format] as string;
|
|
||||||
};
|
|
||||||
this.updateValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
transform(value: Date | number, other: Date | number = Date.now()): string {
|
|
||||||
this.input = value;
|
|
||||||
this.base = other;
|
|
||||||
this.updateValue();
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateValue() {
|
|
||||||
if (!this.input || !this.base) return;
|
|
||||||
this.value = formatRelative(this.input, this.base, {locale: this.locale});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import {formatRelative, getDefaultOptions} from 'date-fns';
|
||||||
|
import {LocaleExtension} from '../dfns-locale';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see {formatRelative}
|
||||||
|
*/
|
||||||
|
export function formatRelativeDate(
|
||||||
|
date: Date | number,
|
||||||
|
baseDate: Date | number,
|
||||||
|
options?: {
|
||||||
|
locale?: LocaleExtension;
|
||||||
|
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
},
|
||||||
|
): string {
|
||||||
|
const locale = options?.locale ?? (getDefaultOptions() as {locale: LocaleExtension}).locale;
|
||||||
|
const customLocale: LocaleExtension = {
|
||||||
|
...locale,
|
||||||
|
formatRelative(token: keyof LocaleExtension['formatRelativeDateOptions']) {
|
||||||
|
return locale.formatRelativeDateOptions![token];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return formatRelative(date, baseDate, {locale: customLocale, weekStartsOn: options?.weekStartsOn});
|
||||||
|
}
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import {OnDestroy, Pipe, PipeTransform} from '@angular/core';
|
|
||||||
import {Subscription} from 'rxjs';
|
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
|
||||||
import {formatDuration} from 'date-fns';
|
|
||||||
|
|
||||||
@Pipe({
|
|
||||||
name: 'dfnsFormatRelativeLive',
|
|
||||||
pure: false,
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
export class FormatRelativeLivePipe implements PipeTransform, OnDestroy {
|
|
||||||
locale: string;
|
|
||||||
|
|
||||||
onLangChange?: Subscription;
|
|
||||||
|
|
||||||
value: string;
|
|
||||||
|
|
||||||
constructor(private readonly translate: TranslateService) {
|
|
||||||
this.locale = translate.currentLang;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _dispose(): void {
|
|
||||||
if (this.onLangChange?.closed === false) {
|
|
||||||
this.onLangChange?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this._dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param value An ISO 8601 duration string
|
|
||||||
*/
|
|
||||||
transform(value: Duration): string {
|
|
||||||
this.updateValue(value);
|
|
||||||
this._dispose();
|
|
||||||
if (this.onLangChange?.closed === true) {
|
|
||||||
this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.locale = event.lang;
|
|
||||||
this.updateValue(value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateValue(value: Duration): void {
|
|
||||||
const fequencyPrefix = Object.keys(this.frequencyPrefixes).filter(element =>
|
|
||||||
this.locale.includes(element),
|
|
||||||
);
|
|
||||||
this.value = [
|
|
||||||
fequencyPrefix.length > 0 ? this.frequencyPrefixes[fequencyPrefix[0]] : this.frequencyPrefixes.en,
|
|
||||||
formatDuration(value),
|
|
||||||
].join(' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import {Injectable, OnDestroy, Pipe, PipeTransform} from '@angular/core';
|
|
||||||
import {Subscription} from 'rxjs';
|
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
|
||||||
import {logger} from '../../_helpers/ts-logger';
|
|
||||||
import opening_hours from 'opening_hours';
|
|
||||||
import {addHours, formatRelative, isBefore, isToday} from 'date-fns';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'openingHours',
|
|
||||||
standalone: true,
|
|
||||||
pure: false,
|
|
||||||
})
|
|
||||||
export class OpeningHoursPipe implements PipeTransform, OnDestroy {
|
|
||||||
locale: string;
|
|
||||||
|
|
||||||
onLangChange?: Subscription;
|
|
||||||
|
|
||||||
value: string[] = [];
|
|
||||||
|
|
||||||
constructor(private readonly translate: TranslateService) {
|
|
||||||
this.locale = translate.currentLang;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _dispose(): void {
|
|
||||||
if (this.onLangChange?.closed === false) {
|
|
||||||
this.onLangChange?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this._dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
transform(aString: string | unknown): string[] {
|
|
||||||
this.updateValue(aString);
|
|
||||||
this._dispose();
|
|
||||||
if (this.onLangChange?.closed === true) {
|
|
||||||
this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.locale = event.lang;
|
|
||||||
this.updateValue(aString);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateValue(aString: string | unknown) {
|
|
||||||
if (typeof aString !== 'string') {
|
|
||||||
logger.warn(`openingHours pipe unable to parse input: ${aString}`);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let openingHours;
|
|
||||||
|
|
||||||
try {
|
|
||||||
openingHours = new opening_hours(aString, {
|
|
||||||
address: {
|
|
||||||
country_code: 'de',
|
|
||||||
state: 'Hessen',
|
|
||||||
},
|
|
||||||
lon: 8.667_97,
|
|
||||||
lat: 50.129_16,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(error);
|
|
||||||
this.value = [];
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isOpen: boolean = openingHours.getState();
|
|
||||||
const isUnknown: boolean = openingHours.getUnknown();
|
|
||||||
|
|
||||||
const nextChange = openingHours.getNextChange();
|
|
||||||
const nextChangeIsOpen: boolean = openingHours.getState(nextChange);
|
|
||||||
const nextChangeUnknown: boolean = openingHours.getUnknown(nextChange);
|
|
||||||
const nextChangeIsToday: boolean = isToday(nextChange!);
|
|
||||||
|
|
||||||
let stateKey = isOpen ? 'common.openingHours.state_open' : 'common.openingHours.state_closed';
|
|
||||||
|
|
||||||
stateKey = isUnknown ? 'common.openingHours.state_maybe' : stateKey;
|
|
||||||
|
|
||||||
this.value = [isOpen ? 'success' : 'danger', `${this.translate.instant(stateKey)}`];
|
|
||||||
|
|
||||||
if (isUnknown) {
|
|
||||||
const comment = openingHours.getComment();
|
|
||||||
this.value = ['light', `${this.translate.instant(stateKey)}`];
|
|
||||||
if (typeof comment === 'string') {
|
|
||||||
this.value.push(comment);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextChangeUnknown) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextChangeKey: string | undefined;
|
|
||||||
|
|
||||||
let formattedCalender = formatRelative(nextChange!, Date.now());
|
|
||||||
|
|
||||||
if (isBefore(nextChange!, addHours(Date.now(), 1))) {
|
|
||||||
this.value[0] = 'warning';
|
|
||||||
nextChangeKey = nextChangeIsOpen
|
|
||||||
? 'common.openingHours.opening_soon_warning'
|
|
||||||
: 'common.openingHours.closing_soon_warning';
|
|
||||||
this.value.push(
|
|
||||||
`${this.translate.instant(nextChangeKey, {
|
|
||||||
time: new Intl.DateTimeFormat(this.locale, {
|
|
||||||
timeStyle: 'short',
|
|
||||||
}).format(nextChange),
|
|
||||||
})}`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextChangeIsToday) {
|
|
||||||
nextChangeKey = nextChangeIsOpen
|
|
||||||
? 'common.openingHours.opening_today'
|
|
||||||
: 'common.openingHours.closing_today';
|
|
||||||
this.value.push(
|
|
||||||
`${this.translate.instant(nextChangeKey, {
|
|
||||||
time: new Intl.DateTimeFormat(this.locale, {
|
|
||||||
timeStyle: 'short',
|
|
||||||
}).format(nextChange),
|
|
||||||
})}`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextChangeKey = nextChangeIsOpen ? 'common.openingHours.opening' : 'common.openingHours.closing';
|
|
||||||
formattedCalender = formattedCalender.slice(0, 1).toUpperCase() + formattedCalender.slice(1);
|
|
||||||
this.value.push(
|
|
||||||
`${this.translate.instant(nextChangeKey, {
|
|
||||||
relativeDateTime: formattedCalender,
|
|
||||||
})}`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,19 @@
|
|||||||
*/
|
*/
|
||||||
import {SCLanguageCode} from '@openstapps/core';
|
import {SCLanguageCode} from '@openstapps/core';
|
||||||
import type {Locale} from 'date-fns';
|
import type {Locale} from 'date-fns';
|
||||||
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
|
|
||||||
|
export interface LocaleExtension extends Locale {
|
||||||
|
formatFrequencyOptions: Partial<Record<Intl.LDMLPluralRule, string>>;
|
||||||
|
formatRelativeDateOptions: {
|
||||||
|
lastWeek: string;
|
||||||
|
yesterday: string;
|
||||||
|
today: string;
|
||||||
|
tomorrow: string;
|
||||||
|
nextWeek: string;
|
||||||
|
other: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
type LocalesMap = Record<SCLanguageCode, () => Promise<{default: Locale}>>;
|
type LocalesMap = Record<SCLanguageCode, () => Promise<{default: Locale}>>;
|
||||||
|
|
||||||
@@ -25,11 +38,23 @@ const LOCALES = {
|
|||||||
/**
|
/**
|
||||||
* Get a Date Fns Locale
|
* Get a Date Fns Locale
|
||||||
*/
|
*/
|
||||||
export async function getDateFnsLocale(code: SCLanguageCode): Promise<Locale> {
|
export async function getDateFnsLocale(
|
||||||
if (code in LOCALES) {
|
code: SCLanguageCode,
|
||||||
return LOCALES[code as keyof typeof LOCALES]().then(it => it.default);
|
translator: TranslateService,
|
||||||
} else {
|
): Promise<LocaleExtension> {
|
||||||
|
if (!(code in LOCALES)) {
|
||||||
console.warn(`Unknown Locale "${code}" for Date Fns. Falling back to English.`);
|
console.warn(`Unknown Locale "${code}" for Date Fns. Falling back to English.`);
|
||||||
return LOCALES.en().then(it => it.default);
|
|
||||||
}
|
}
|
||||||
|
const key = code in LOCALES ? (code as keyof typeof LOCALES) : 'en';
|
||||||
|
const translations = translator.translations[translator.currentLang];
|
||||||
|
const frequencyExtension = translations['dateFns']['FORMAT_FREQUENCY'];
|
||||||
|
const relativeDateExtension = translations['dateFns']['FORMAT_RELATIVE_DATE'];
|
||||||
|
|
||||||
|
return LOCALES[key]().then(it => {
|
||||||
|
const locale = it.default as LocaleExtension;
|
||||||
|
locale.formatFrequencyOptions = {...frequencyExtension};
|
||||||
|
locale.formatRelativeDateOptions = {...relativeDateExtension};
|
||||||
|
|
||||||
|
return locale;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export class ThingTranslateService {
|
|||||||
/** set the default language from configuration */
|
/** set the default language from configuration */
|
||||||
this.translateService.onLangChange.pipe(takeUntilDestroyed()).subscribe((event: LangChangeEvent) => {
|
this.translateService.onLangChange.pipe(takeUntilDestroyed()).subscribe((event: LangChangeEvent) => {
|
||||||
this.translator.language = event.lang as keyof SCTranslations<SCLanguage>;
|
this.translator.language = event.lang as keyof SCTranslations<SCLanguage>;
|
||||||
getDateFnsLocale(event.lang as SCLanguageCode).then(locale => {
|
getDateFnsLocale(event.lang as SCLanguageCode, translateService).then(locale => {
|
||||||
setDefaultOptions({locale});
|
setDefaultOptions({locale});
|
||||||
this.dfnsConfiguration.setLocale(locale);
|
this.dfnsConfiguration.setLocale(locale);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,20 @@
|
|||||||
"export": "Exportieren",
|
"export": "Exportieren",
|
||||||
"share": "Teilen",
|
"share": "Teilen",
|
||||||
"timeSuffix": "Uhr",
|
"timeSuffix": "Uhr",
|
||||||
|
"dateFns": {
|
||||||
|
"FORMAT_FREQUENCY": {
|
||||||
|
"one": "'jede' {{suffix}}",
|
||||||
|
"many": "'alle' {{duration}}"
|
||||||
|
},
|
||||||
|
"FORMAT_RELATIVE_DATE": {
|
||||||
|
"lastWeek": "'letzten' eeee",
|
||||||
|
"yesterday": "'gestern'",
|
||||||
|
"today": "'heute'",
|
||||||
|
"tomorrow": "'morgen'",
|
||||||
|
"nextWeek": "'nächsten' eeee",
|
||||||
|
"other": "pp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ratings": {
|
"ratings": {
|
||||||
"thank_you": "Vielen Dank für die Bewertung!"
|
"thank_you": "Vielen Dank für die Bewertung!"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,20 @@
|
|||||||
"export": "Export",
|
"export": "Export",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"timeSuffix": "",
|
"timeSuffix": "",
|
||||||
|
"dateFns": {
|
||||||
|
"FORMAT_FREQUENCY": {
|
||||||
|
"one": "'every' {{suffix}}",
|
||||||
|
"many": "'every' {{duration}}"
|
||||||
|
},
|
||||||
|
"FORMAT_RELATIVE_DATE": {
|
||||||
|
"lastWeek": "'last' eeee",
|
||||||
|
"yesterday": "'yesterday'",
|
||||||
|
"today": "'today'",
|
||||||
|
"tomorrow": "'tomorrow'",
|
||||||
|
"nextWeek": "'next' eeee",
|
||||||
|
"other": "pp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ratings": {
|
"ratings": {
|
||||||
"thank_you": "Thank you for your feedback!"
|
"thank_you": "Thank you for your feedback!"
|
||||||
},
|
},
|
||||||
|
|||||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -842,6 +842,9 @@ importers:
|
|||||||
deepmerge:
|
deepmerge:
|
||||||
specifier: 4.3.1
|
specifier: 4.3.1
|
||||||
version: 4.3.1
|
version: 4.3.1
|
||||||
|
duration-fns:
|
||||||
|
specifier: 3.0.2
|
||||||
|
version: 3.0.2
|
||||||
form-data:
|
form-data:
|
||||||
specifier: 4.0.0
|
specifier: 4.0.0
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
@@ -863,9 +866,6 @@ importers:
|
|||||||
material-symbols:
|
material-symbols:
|
||||||
specifier: 0.10.0
|
specifier: 0.10.0
|
||||||
version: 0.10.0
|
version: 0.10.0
|
||||||
moment:
|
|
||||||
specifier: 2.29.4
|
|
||||||
version: 2.29.4
|
|
||||||
ngx-date-fns:
|
ngx-date-fns:
|
||||||
specifier: 10.0.1
|
specifier: 10.0.1
|
||||||
version: 10.0.1(@angular/common@16.1.4)(@angular/core@16.1.4)(date-fns@2.30.0)
|
version: 10.0.1(@angular/common@16.1.4)(@angular/core@16.1.4)(date-fns@2.30.0)
|
||||||
@@ -875,9 +875,6 @@ importers:
|
|||||||
ngx-markdown:
|
ngx-markdown:
|
||||||
specifier: 16.0.0
|
specifier: 16.0.0
|
||||||
version: 16.0.0(@angular/common@16.1.4)(@angular/core@16.1.4)(@angular/platform-browser@16.1.4)(@types/marked@4.3.1)(marked@4.3.0)(rxjs@7.8.1)(zone.js@0.13.1)
|
version: 16.0.0(@angular/common@16.1.4)(@angular/core@16.1.4)(@angular/platform-browser@16.1.4)(@types/marked@4.3.1)(marked@4.3.0)(rxjs@7.8.1)(zone.js@0.13.1)
|
||||||
ngx-moment:
|
|
||||||
specifier: 6.0.2
|
|
||||||
version: 6.0.2(moment@2.29.4)
|
|
||||||
opening_hours:
|
opening_hours:
|
||||||
specifier: 3.8.0
|
specifier: 3.8.0
|
||||||
version: 3.8.0
|
version: 3.8.0
|
||||||
@@ -10328,6 +10325,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
|
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/duration-fns@3.0.2:
|
||||||
|
resolution: {integrity: sha512-w82IXh/6aWNHFA0qlQazJYJrZTWieTItuuGTE7YX4cxPaZTWhmVImbsBBiMK1/OhGDgiinuCpJoSFILYLDSKDg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/eastasianwidth@0.2.0:
|
/eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
@@ -14333,15 +14334,6 @@ packages:
|
|||||||
prismjs: 1.29.0
|
prismjs: 1.29.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/ngx-moment@6.0.2(moment@2.29.4):
|
|
||||||
resolution: {integrity: sha512-HUvDyoJPZKLA3tc+GMQqDpVyCVT2SPfEiV7/CGj2Dwwsn//JhhQ8eTr+RzKqBzLysrXkCwlzulVVJaJ5A0FJEA==}
|
|
||||||
peerDependencies:
|
|
||||||
moment: ^2.19.3
|
|
||||||
dependencies:
|
|
||||||
moment: 2.29.4
|
|
||||||
tslib: 2.4.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/nice-napi@1.0.2:
|
/nice-napi@1.0.2:
|
||||||
resolution: {integrity: sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==}
|
resolution: {integrity: sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==}
|
||||||
os: ['!win32']
|
os: ['!win32']
|
||||||
|
|||||||
Reference in New Issue
Block a user