Localized Dates (#2182)

* Removed 4 properties from SiteThemeDto which weren't supposed to be there.

* Removed another set of date fields not used on DTOs

* Hangfire jobs will now grab a utc date and render that date in user's local timezone.

* Scrobble errors are now localized dates.

Added simplified chinese language code

* Fixed a bunch of newlines in the translation files

* Localized compact number and fixed some missing localizations

* Fixed remove from on deck key issue

* Scrobble events is now localized

* Scrobble events is now localized

* Removed some duplicate fields from chapter
This commit is contained in:
Joe Milazzo 2023-08-05 14:02:35 -05:00 committed by GitHub
parent c3b3f9a640
commit a65963c817
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 99 additions and 150 deletions

View file

@ -21,7 +21,7 @@ export interface Chapter {
pagesRead: number; // Attached for the given user when requesting from API
isSpecial: boolean;
title: string;
created: string;
createdUtc: string;
/**
* Actual name of the Chapter if populated in underlying metadata
*/

View file

@ -6,4 +6,5 @@ export interface Device {
platform: DevicePlatform;
emailAddress: string;
lastUsed: string;
}
lastUsedUtc: string;
}

View file

@ -2,6 +2,5 @@ export interface Job {
id: string;
title: string;
cron: string;
createdAt: string;
lastExecution: string;
}
lastExecutionUtc: string;
}

View file

@ -14,8 +14,8 @@ export interface ScrobbleEvent {
scrobbleEventType: ScrobbleEventType;
rating: number | null;
processedDateUtc: string;
lastModified: string;
created: string;
lastModifiedUtc: string;
createdUtc: string;
volumeNumber: number | null;
chapterNumber: number | null;
}

View file

@ -32,8 +32,8 @@ export class AccountService {
private readonly destroyRef = inject(DestroyRef);
baseUrl = environment.apiUrl;
userKey = 'kavita-user';
public lastLoginKey = 'kavita-lastlogin';
public localeKey = 'kavita-locale';
public static lastLoginKey = 'kavita-lastlogin';
public static localeKey = 'kavita-locale';
private currentUser: User | undefined;
// Stores values, when someone subscribes gives (1) of last values seen.
@ -135,7 +135,7 @@ export class AccountService {
Array.isArray(roles) ? user.roles = roles : user.roles.push(roles);
localStorage.setItem(this.userKey, JSON.stringify(user));
localStorage.setItem(this.lastLoginKey, user.username);
localStorage.setItem(AccountService.lastLoginKey, user.username);
if (user.preferences && user.preferences.theme) {
this.themeService.setTheme(user.preferences.theme.name);
} else {
@ -268,8 +268,8 @@ export class AccountService {
this.currentUser.preferences = settings;
this.setCurrentUser(this.currentUser);
// Update the locale on disk (for logout only)
localStorage.setItem(this.localeKey, this.currentUser.preferences.locale);
// Update the locale on disk (for logout and compact-number pipe)
localStorage.setItem(AccountService.localeKey, this.currentUser.preferences.locale);
}
return settings;
}), takeUntilDestroyed(this.destroyRef));

View file

@ -22,10 +22,10 @@
<table class="table table-striped table-hover table-sm scrollable">
<thead>
<tr>
<th scope="col" sortable="created" (sort)="updateSort($event)">
<th scope="col" sortable="createdUtc" (sort)="updateSort($event)">
{{t('created-header')}}
</th>
<th scope="col" sortable="lastModified" (sort)="updateSort($event)" direction="desc">
<th scope="col" sortable="lastModifiedUtc" (sort)="updateSort($event)" direction="desc">
{{t('last-modified-header')}}
</th>
<th scope="col">
@ -48,10 +48,10 @@
</tr>
<tr *ngFor="let item of events; let idx = index;">
<td>
{{item.created | date:'MM/dd/yy h:mm a' }}
{{item.createdUtc | translocoDate: {dateStyle: 'short', timeStyle: 'medium', } | defaultValue }}
</td>
<td>
{{item.lastModified | date:'MM/dd/yy h:mm a' }}
{{item.lastModifiedUtc | translocoDate: {dateStyle: 'short', timeStyle: 'medium' } | defaultValue }}
</td>
<td>
{{item.scrobbleEventType | scrobbleEventType}}

View file

@ -12,11 +12,13 @@ import {PaginatedResult, Pagination} from "../../_models/pagination";
import {SortableHeader, SortEvent} from "../table/_directives/sortable-header.directive";
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
import {TranslocoModule} from "@ngneat/transloco";
import {DefaultValuePipe} from "../../pipe/default-value.pipe";
import {TranslocoLocaleModule} from "@ngneat/transloco-locale";
@Component({
selector: 'app-user-scrobble-history',
standalone: true,
imports: [CommonModule, ScrobbleEventTypePipe, NgbPagination, ReactiveFormsModule, SortableHeader, TranslocoModule],
imports: [CommonModule, ScrobbleEventTypePipe, NgbPagination, ReactiveFormsModule, SortableHeader, TranslocoModule, DefaultValuePipe, TranslocoLocaleModule],
templateUrl: './user-scrobble-history.component.html',
styleUrls: ['./user-scrobble-history.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
@ -36,7 +38,7 @@ export class UserScrobbleHistoryComponent implements OnInit {
get ScrobbleEventType() { return ScrobbleEventType; }
ngOnInit() {
this.loadPage({column: 'created', direction: 'desc'});
this.loadPage({column: 'createdUtc', direction: 'desc'});
this.formGroup.get('filter')?.valueChanges.pipe(debounceTime(200), takeUntilDestroyed(this.destroyRef)).subscribe(query => {
this.loadPage();
@ -81,9 +83,9 @@ export class UserScrobbleHistoryComponent implements OnInit {
private mapSortColumnField(column: string | undefined) {
switch (column) {
case 'created': return ScrobbleEventSortField.Created;
case 'createdUtc': return ScrobbleEventSortField.Created;
case 'isProcessed': return ScrobbleEventSortField.IsProcessed;
case 'lastModified': return ScrobbleEventSortField.LastModified;
case 'lastModifiedUtc': return ScrobbleEventSortField.LastModified;
case 'seriesName': return ScrobbleEventSortField.Series;
}
return ScrobbleEventSortField.None;

View file

@ -3,6 +3,4 @@ export interface KavitaMediaError {
filePath: string;
comment: string;
details: string;
created: string;
createdUtc: string;
}
}

View file

@ -38,7 +38,7 @@
<a href="library/{{item.libraryId}}/series/{{item.seriesId}}" target="_blank">{{item.details}}</a>
</td>
<td>
{{item.created | date:'shortDate'}}
{{item.createdUtc | translocoDate: {dateStyle: 'short', timeStyle: 'medium' } | defaultValue }}
</td>
<td>
{{item.comment}}

View file

@ -26,11 +26,14 @@ import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
import {FilterPipe} from "../../pipe/filter.pipe";
import {LoadingComponent} from "../../shared/loading/loading.component";
import {TranslocoModule} from "@ngneat/transloco";
import {DefaultDatePipe} from "../../pipe/default-date.pipe";
import {DefaultValuePipe} from "../../pipe/default-value.pipe";
import {TranslocoLocaleModule} from "@ngneat/transloco-locale";
@Component({
selector: 'app-manage-scrobble-errors',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, FilterPipe, LoadingComponent, SortableHeader, TranslocoModule],
imports: [CommonModule, ReactiveFormsModule, FilterPipe, LoadingComponent, SortableHeader, TranslocoModule, DefaultDatePipe, DefaultValuePipe, TranslocoLocaleModule],
templateUrl: './manage-scrobble-errors.component.html',
styleUrls: ['./manage-scrobble-errors.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush

View file

@ -58,7 +58,7 @@
<td>
{{task.title | titlecase}}
</td>
<td>{{task.lastExecution | date:'short' | defaultValue }}</td>
<td>{{task.lastExecutionUtc | translocoDate: {dateStyle: 'short', timeStyle: 'medium' } | defaultValue }}</td>
<td>{{task.cron}}</td>
</tr>
</tbody>

View file

@ -13,6 +13,7 @@ import {DownloadService} from 'src/app/shared/_services/download.service';
import {DefaultValuePipe} from '../../pipe/default-value.pipe';
import {AsyncPipe, DatePipe, NgFor, NgIf, NgTemplateOutlet, TitleCasePipe} from '@angular/common';
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
import {TranslocoLocaleModule} from "@ngneat/transloco-locale";
interface AdhocTask {
name: string;
@ -28,7 +29,7 @@ interface AdhocTask {
styleUrls: ['./manage-tasks-settings.component.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgFor, AsyncPipe, TitleCasePipe, DatePipe, DefaultValuePipe, TranslocoModule, NgTemplateOutlet]
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgFor, AsyncPipe, TitleCasePipe, DatePipe, DefaultValuePipe, TranslocoModule, NgTemplateOutlet, TranslocoLocaleModule]
})
export class ManageTasksSettingsComponent implements OnInit {

View file

@ -33,7 +33,7 @@
<ng-container *ngIf="totalPages > 0">
<div class="col-auto mb-2">
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-regular fa-file-lines" [title]="t('pages-title')">
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-regular fa-file-lines">
{{t('pages-count', {num: totalPages | compactNumber})}}
</app-icon-and-title>
</div>
@ -60,11 +60,11 @@
</div>
</ng-container>
<ng-container *ngIf="showExtendedProperties && chapter.created && chapter.created !== '' && (chapter.created | date: 'shortDate') !== '1/1/01'">
<ng-container *ngIf="showExtendedProperties && chapter.createdUtc && chapter.createdUtc !== '' && (chapter.createdUtc | date: 'shortDate') !== '1/1/01'">
<div class="vr d-none d-lg-block m-2"></div>
<div class="col-auto">
<app-icon-and-title [label]="t('date-added-title')" [clickable]="false" fontClasses="fa-solid fa-file-import" [title]="t('date-added-title')">
{{chapter.created | date:'short' | defaultDate}}
{{chapter.createdUtc | translocoDate: {dateStyle: 'short', timeStyle: 'short' } | defaultDate}}
</app-icon-and-title>
</div>
</ng-container>

View file

@ -27,11 +27,12 @@ import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
import {MetadataDetailComponent} from "../../series-detail/_components/metadata-detail/metadata-detail.component";
import {FilterQueryParam} from "../../shared/_services/filter-utilities.service";
import {TranslocoModule} from "@ngneat/transloco";
import {TranslocoLocaleModule} from "@ngneat/transloco-locale";
@Component({
selector: 'app-entity-info-cards',
standalone: true,
imports: [CommonModule, IconAndTitleComponent, SafeHtmlPipe, DefaultDatePipe, BytesPipe, CompactNumberPipe, AgeRatingPipe, NgbTooltip, MetadataDetailComponent, TranslocoModule],
imports: [CommonModule, IconAndTitleComponent, SafeHtmlPipe, DefaultDatePipe, BytesPipe, CompactNumberPipe, AgeRatingPipe, NgbTooltip, MetadataDetailComponent, TranslocoModule, CompactNumberPipe, TranslocoLocaleModule],
templateUrl: './entity-info-cards.component.html',
styleUrls: ['./entity-info-cards.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush

View file

@ -91,7 +91,7 @@ export class SeriesCardComponent implements OnInit, OnChanges {
if (this.actions[othersIndex].children.findIndex(o => o.action === Action.RemoveFromOnDeck) < 0) {
this.actions[othersIndex].children.push({
action: Action.RemoveFromOnDeck,
title: this.translocoService.translate('actionable.remove-from-on-deck'),
title: 'remove-from-on-deck',
callback: (action: ActionItem<Series>, series: Series) => this.handleSeriesActionCallback(action, series),
class: 'danger',
requiresAdmin: false,

View file

@ -1,17 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core';
// TODO: Figure out how to handle culture based on localization setting
const formatter = new Intl.NumberFormat('en-GB', {
//@ts-ignore
notation: 'compact', // https://github.com/microsoft/TypeScript/issues/36533
maximumSignificantDigits: 3
});
const formatterForDoublePercision = new Intl.NumberFormat('en-GB', {
//@ts-ignore
notation: 'compact', // https://github.com/microsoft/TypeScript/issues/36533
maximumSignificantDigits: 2
});
import {AccountService} from "../_services/account.service";
const specialCases = [4, 7, 10, 13];
@ -21,14 +9,35 @@ const specialCases = [4, 7, 10, 13];
})
export class CompactNumberPipe implements PipeTransform {
constructor() {}
transform(value: number): string {
// Weblate allows some non-standard languages, like 'zh_Hans', which should be just 'zh'. So we handle that here
const locale = localStorage.getItem(AccountService.localeKey)?.split('_')[0];
return this.transformValue(locale || 'en', value);
}
private transformValue(locale: string, value: number) {
const formatter = new Intl.NumberFormat(locale, {
//@ts-ignore
notation: 'compact', // https://github.com/microsoft/TypeScript/issues/36533
maximumSignificantDigits: 3
});
const formatterForDoublePrecision = new Intl.NumberFormat(locale, {
//@ts-ignore
notation: 'compact', // https://github.com/microsoft/TypeScript/issues/36533
maximumSignificantDigits: 2
});
if (value < 1000) return value + '';
if (specialCases.includes((value + '').length)) { // from 4, every 3 will have a case where we need to override
return formatterForDoublePercision.format(value);
return formatterForDoublePrecision.format(value);
}
return formatter.format(value);
}
}

View file

@ -3,7 +3,7 @@
<div class="dashboard-card-content">
<div class="row g-0 mb-2 align-items-center">
<div class="col-4">
<h4>{{t('reading-activity')}}</h4>
<h4>{{t('title')}}</h4>
</div>
<div class="col-8">
<form [formGroup]="formGroup" class="d-inline-flex float-end">

View file

@ -31,7 +31,9 @@ import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
styleUrls: ['./server-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgIf, IconAndTitleComponent, StatListComponent, TopReadersComponent, FileBreakdownStatsComponent, PublicationStatusStatsComponent, ReadingActivityComponent, DayBreakdownComponent, AsyncPipe, DecimalPipe, CompactNumberPipe, TimeDurationPipe, BytesPipe, TranslocoModule]
imports: [NgIf, IconAndTitleComponent, StatListComponent, TopReadersComponent, FileBreakdownStatsComponent,
PublicationStatusStatsComponent, ReadingActivityComponent, DayBreakdownComponent, AsyncPipe, DecimalPipe,
CompactNumberPipe, TimeDurationPipe, BytesPipe, TranslocoModule]
})
export class ServerStatsComponent {

View file

@ -1,14 +1,14 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { CompactNumberPipe } from 'src/app/pipe/compact-number.pipe';
import { StatisticsService } from 'src/app/_services/statistics.service';
import { GenericListModalComponent } from '../_modals/generic-list-modal/generic-list-modal.component';
import { TimeAgoPipe } from '../../../pipe/time-ago.pipe';
import { TimeDurationPipe } from '../../../pipe/time-duration.pipe';
import { CompactNumberPipe as CompactNumberPipe_1 } from '../../../pipe/compact-number.pipe';
import { DecimalPipe } from '@angular/common';
import { IconAndTitleComponent } from '../../../shared/icon-and-title/icon-and-title.component';
import {TranslocoModule} from "@ngneat/transloco";
import {AccountService} from "../../../_services/account.service";
import {CompactNumberPipe} from "../../../pipe/compact-number.pipe";
@Component({
selector: 'app-user-stats-info-cards',
@ -16,7 +16,7 @@ import {TranslocoModule} from "@ngneat/transloco";
styleUrls: ['./user-stats-info-cards.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [IconAndTitleComponent, DecimalPipe, CompactNumberPipe_1, TimeDurationPipe, TimeAgoPipe, TranslocoModule]
imports: [IconAndTitleComponent, DecimalPipe, CompactNumberPipe, TimeDurationPipe, TimeAgoPipe, TranslocoModule]
})
export class UserStatsInfoCardsComponent {
@ -27,7 +27,7 @@ export class UserStatsInfoCardsComponent {
@Input() lastActive: string = '';
@Input() avgHoursPerWeekSpentReading: number = 0;
constructor(private statsService: StatisticsService, private modalService: NgbModal) { }
constructor(private statsService: StatisticsService, private modalService: NgbModal, private accountService: AccountService) { }
openPageByYearList() {
const numberPipe = new CompactNumberPipe();
@ -46,5 +46,4 @@ export class UserStatsInfoCardsComponent {
ref.componentInstance.title = 'Words Read By Year';
});
}
}