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

@ -222,8 +222,7 @@ public class ServerController : BaseApiController
Id = dto.Id, Id = dto.Id,
Title = dto.Id.Replace('-', ' '), Title = dto.Id.Replace('-', ' '),
Cron = dto.Cron, Cron = dto.Cron,
CreatedAt = dto.CreatedAt, LastExecutionUtc = dto.LastExecution.HasValue ? new DateTime(dto.LastExecution.Value.Ticks, DateTimeKind.Utc) : null
LastExecution = dto.LastExecution,
}); });
return Ok(recurringJobs); return Ok(recurringJobs);

View File

@ -9,7 +9,7 @@ namespace API.DTOs;
/// A Chapter is the lowest grouping of a reading medium. A Chapter contains a set of MangaFiles which represents the underlying /// A Chapter is the lowest grouping of a reading medium. A Chapter contains a set of MangaFiles which represents the underlying
/// file (abstracted from type). /// file (abstracted from type).
/// </summary> /// </summary>
public class ChapterDto : IHasReadTimeEstimate, IEntityDate public class ChapterDto : IHasReadTimeEstimate
{ {
public int Id { get; init; } public int Id { get; init; }
/// <summary> /// <summary>
@ -59,8 +59,6 @@ public class ChapterDto : IHasReadTimeEstimate, IEntityDate
/// <summary> /// <summary>
/// When chapter was created /// When chapter was created
/// </summary> /// </summary>
public DateTime Created { get; set; }
public DateTime LastModified { get; set; }
public DateTime CreatedUtc { get; set; } public DateTime CreatedUtc { get; set; }
public DateTime LastModifiedUtc { get; set; } public DateTime LastModifiedUtc { get; set; }
/// <summary> /// <summary>

View File

@ -26,12 +26,4 @@ public class DeviceDto
/// Platform (ie) Windows 10 /// Platform (ie) Windows 10
/// </summary> /// </summary>
public DevicePlatform Platform { get; set; } public DevicePlatform Platform { get; set; }
/// <summary>
/// Last time this device was used to send a file
/// </summary>
public DateTime LastUsed { get; set; }
/// <summary>
/// Last time this device was used to send a file
/// </summary>
public DateTime LastUsedUtc { get; set; }
} }

View File

@ -15,14 +15,6 @@ public class JobDto
/// <summary> /// <summary>
/// When the job was created /// When the job was created
/// </summary> /// </summary>
public DateTime? CreatedAt { get; set; }
/// <summary>
/// Last time the job was run
/// </summary>
public DateTime? LastExecution { get; set; }
/// <summary>
/// When the job was created
/// </summary>
public DateTime? CreatedAtUtc { get; set; } public DateTime? CreatedAtUtc { get; set; }
/// <summary> /// <summary>
/// Last time the job was run /// Last time the job was run

View File

@ -20,6 +20,4 @@ public class MediaErrorDto
/// Exception message /// Exception message
/// </summary> /// </summary>
public string Details { get; set; } public string Details { get; set; }
public DateTime Created { get; set; }
public DateTime CreatedUtc { get; set; }
} }

View File

@ -10,8 +10,8 @@ public class ScrobbleEventDto
public bool IsProcessed { get; set; } public bool IsProcessed { get; set; }
public int? VolumeNumber { get; set; } public int? VolumeNumber { get; set; }
public int? ChapterNumber { get; set; } public int? ChapterNumber { get; set; }
public DateTime LastModified { get; set; } public DateTime LastModifiedUtc { get; set; }
public DateTime Created { get; set; } public DateTime CreatedUtc { get; set; }
public float? Rating { get; set; } public float? Rating { get; set; }
public ScrobbleEventType ScrobbleEventType { get; set; } public ScrobbleEventType ScrobbleEventType { get; set; }

View File

@ -1,6 +1,4 @@
using System;
using API.Entities.Enums.Theme; using API.Entities.Enums.Theme;
using API.Entities.Interfaces;
using API.Services; using API.Services;
namespace API.DTOs.Theme; namespace API.DTOs.Theme;
@ -8,7 +6,7 @@ namespace API.DTOs.Theme;
/// <summary> /// <summary>
/// Represents a set of css overrides the user can upload to Kavita and will load into webui /// Represents a set of css overrides the user can upload to Kavita and will load into webui
/// </summary> /// </summary>
public class SiteThemeDto : IEntityDate public class SiteThemeDto
{ {
public int Id { get; set; } public int Id { get; set; }
/// <summary> /// <summary>
@ -32,9 +30,5 @@ public class SiteThemeDto : IEntityDate
/// Where did the theme come from /// Where did the theme come from
/// </summary> /// </summary>
public ThemeProvider Provider { get; set; } public ThemeProvider Provider { get; set; }
public DateTime Created { get; set; }
public DateTime LastModified { get; set; }
public DateTime CreatedUtc { get; set; }
public DateTime LastModifiedUtc { get; set; }
public string Selector => "bg-" + Name.ToLower(); public string Selector => "bg-" + Name.ToLower();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,10 +22,10 @@
<table class="table table-striped table-hover table-sm scrollable"> <table class="table table-striped table-hover table-sm scrollable">
<thead> <thead>
<tr> <tr>
<th scope="col" sortable="created" (sort)="updateSort($event)"> <th scope="col" sortable="createdUtc" (sort)="updateSort($event)">
{{t('created-header')}} {{t('created-header')}}
</th> </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')}} {{t('last-modified-header')}}
</th> </th>
<th scope="col"> <th scope="col">
@ -48,10 +48,10 @@
</tr> </tr>
<tr *ngFor="let item of events; let idx = index;"> <tr *ngFor="let item of events; let idx = index;">
<td> <td>
{{item.created | date:'MM/dd/yy h:mm a' }} {{item.createdUtc | translocoDate: {dateStyle: 'short', timeStyle: 'medium', } | defaultValue }}
</td> </td>
<td> <td>
{{item.lastModified | date:'MM/dd/yy h:mm a' }} {{item.lastModifiedUtc | translocoDate: {dateStyle: 'short', timeStyle: 'medium' } | defaultValue }}
</td> </td>
<td> <td>
{{item.scrobbleEventType | scrobbleEventType}} {{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 {SortableHeader, SortEvent} from "../table/_directives/sortable-header.directive";
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms"; import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
import {TranslocoModule} from "@ngneat/transloco"; import {TranslocoModule} from "@ngneat/transloco";
import {DefaultValuePipe} from "../../pipe/default-value.pipe";
import {TranslocoLocaleModule} from "@ngneat/transloco-locale";
@Component({ @Component({
selector: 'app-user-scrobble-history', selector: 'app-user-scrobble-history',
standalone: true, standalone: true,
imports: [CommonModule, ScrobbleEventTypePipe, NgbPagination, ReactiveFormsModule, SortableHeader, TranslocoModule], imports: [CommonModule, ScrobbleEventTypePipe, NgbPagination, ReactiveFormsModule, SortableHeader, TranslocoModule, DefaultValuePipe, TranslocoLocaleModule],
templateUrl: './user-scrobble-history.component.html', templateUrl: './user-scrobble-history.component.html',
styleUrls: ['./user-scrobble-history.component.scss'], styleUrls: ['./user-scrobble-history.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
@ -36,7 +38,7 @@ export class UserScrobbleHistoryComponent implements OnInit {
get ScrobbleEventType() { return ScrobbleEventType; } get ScrobbleEventType() { return ScrobbleEventType; }
ngOnInit() { 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.formGroup.get('filter')?.valueChanges.pipe(debounceTime(200), takeUntilDestroyed(this.destroyRef)).subscribe(query => {
this.loadPage(); this.loadPage();
@ -81,9 +83,9 @@ export class UserScrobbleHistoryComponent implements OnInit {
private mapSortColumnField(column: string | undefined) { private mapSortColumnField(column: string | undefined) {
switch (column) { switch (column) {
case 'created': return ScrobbleEventSortField.Created; case 'createdUtc': return ScrobbleEventSortField.Created;
case 'isProcessed': return ScrobbleEventSortField.IsProcessed; case 'isProcessed': return ScrobbleEventSortField.IsProcessed;
case 'lastModified': return ScrobbleEventSortField.LastModified; case 'lastModifiedUtc': return ScrobbleEventSortField.LastModified;
case 'seriesName': return ScrobbleEventSortField.Series; case 'seriesName': return ScrobbleEventSortField.Series;
} }
return ScrobbleEventSortField.None; return ScrobbleEventSortField.None;

View File

@ -3,6 +3,4 @@ export interface KavitaMediaError {
filePath: string; filePath: string;
comment: string; comment: string;
details: 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> <a href="library/{{item.libraryId}}/series/{{item.seriesId}}" target="_blank">{{item.details}}</a>
</td> </td>
<td> <td>
{{item.created | date:'shortDate'}} {{item.createdUtc | translocoDate: {dateStyle: 'short', timeStyle: 'medium' } | defaultValue }}
</td> </td>
<td> <td>
{{item.comment}} {{item.comment}}

View File

@ -26,11 +26,14 @@ import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
import {FilterPipe} from "../../pipe/filter.pipe"; import {FilterPipe} from "../../pipe/filter.pipe";
import {LoadingComponent} from "../../shared/loading/loading.component"; import {LoadingComponent} from "../../shared/loading/loading.component";
import {TranslocoModule} from "@ngneat/transloco"; 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({ @Component({
selector: 'app-manage-scrobble-errors', selector: 'app-manage-scrobble-errors',
standalone: true, 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', templateUrl: './manage-scrobble-errors.component.html',
styleUrls: ['./manage-scrobble-errors.component.scss'], styleUrls: ['./manage-scrobble-errors.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush

View File

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

View File

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

View File

@ -33,7 +33,7 @@
<ng-container *ngIf="totalPages > 0"> <ng-container *ngIf="totalPages > 0">
<div class="col-auto mb-2"> <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})}} {{t('pages-count', {num: totalPages | compactNumber})}}
</app-icon-and-title> </app-icon-and-title>
</div> </div>
@ -60,11 +60,11 @@
</div> </div>
</ng-container> </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="vr d-none d-lg-block m-2"></div>
<div class="col-auto"> <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')"> <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> </app-icon-and-title>
</div> </div>
</ng-container> </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 {MetadataDetailComponent} from "../../series-detail/_components/metadata-detail/metadata-detail.component";
import {FilterQueryParam} from "../../shared/_services/filter-utilities.service"; import {FilterQueryParam} from "../../shared/_services/filter-utilities.service";
import {TranslocoModule} from "@ngneat/transloco"; import {TranslocoModule} from "@ngneat/transloco";
import {TranslocoLocaleModule} from "@ngneat/transloco-locale";
@Component({ @Component({
selector: 'app-entity-info-cards', selector: 'app-entity-info-cards',
standalone: true, 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', templateUrl: './entity-info-cards.component.html',
styleUrls: ['./entity-info-cards.component.scss'], styleUrls: ['./entity-info-cards.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush 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) { if (this.actions[othersIndex].children.findIndex(o => o.action === Action.RemoveFromOnDeck) < 0) {
this.actions[othersIndex].children.push({ this.actions[othersIndex].children.push({
action: Action.RemoveFromOnDeck, 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), callback: (action: ActionItem<Series>, series: Series) => this.handleSeriesActionCallback(action, series),
class: 'danger', class: 'danger',
requiresAdmin: false, requiresAdmin: false,

View File

@ -1,17 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import {AccountService} from "../_services/account.service";
// 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
});
const specialCases = [4, 7, 10, 13]; const specialCases = [4, 7, 10, 13];
@ -21,14 +9,35 @@ const specialCases = [4, 7, 10, 13];
}) })
export class CompactNumberPipe implements PipeTransform { export class CompactNumberPipe implements PipeTransform {
constructor() {}
transform(value: number): string { 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 (value < 1000) return value + '';
if (specialCases.includes((value + '').length)) { // from 4, every 3 will have a case where we need to override 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); return formatter.format(value);
} }
} }

View File

@ -3,7 +3,7 @@
<div class="dashboard-card-content"> <div class="dashboard-card-content">
<div class="row g-0 mb-2 align-items-center"> <div class="row g-0 mb-2 align-items-center">
<div class="col-4"> <div class="col-4">
<h4>{{t('reading-activity')}}</h4> <h4>{{t('title')}}</h4>
</div> </div>
<div class="col-8"> <div class="col-8">
<form [formGroup]="formGroup" class="d-inline-flex float-end"> <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'], styleUrls: ['./server-stats.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, 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 { 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 { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { CompactNumberPipe } from 'src/app/pipe/compact-number.pipe';
import { StatisticsService } from 'src/app/_services/statistics.service'; import { StatisticsService } from 'src/app/_services/statistics.service';
import { GenericListModalComponent } from '../_modals/generic-list-modal/generic-list-modal.component'; import { GenericListModalComponent } from '../_modals/generic-list-modal/generic-list-modal.component';
import { TimeAgoPipe } from '../../../pipe/time-ago.pipe'; import { TimeAgoPipe } from '../../../pipe/time-ago.pipe';
import { TimeDurationPipe } from '../../../pipe/time-duration.pipe'; import { TimeDurationPipe } from '../../../pipe/time-duration.pipe';
import { CompactNumberPipe as CompactNumberPipe_1 } from '../../../pipe/compact-number.pipe';
import { DecimalPipe } from '@angular/common'; import { DecimalPipe } from '@angular/common';
import { IconAndTitleComponent } from '../../../shared/icon-and-title/icon-and-title.component'; import { IconAndTitleComponent } from '../../../shared/icon-and-title/icon-and-title.component';
import {TranslocoModule} from "@ngneat/transloco"; import {TranslocoModule} from "@ngneat/transloco";
import {AccountService} from "../../../_services/account.service";
import {CompactNumberPipe} from "../../../pipe/compact-number.pipe";
@Component({ @Component({
selector: 'app-user-stats-info-cards', selector: 'app-user-stats-info-cards',
@ -16,7 +16,7 @@ import {TranslocoModule} from "@ngneat/transloco";
styleUrls: ['./user-stats-info-cards.component.scss'], styleUrls: ['./user-stats-info-cards.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, standalone: true,
imports: [IconAndTitleComponent, DecimalPipe, CompactNumberPipe_1, TimeDurationPipe, TimeAgoPipe, TranslocoModule] imports: [IconAndTitleComponent, DecimalPipe, CompactNumberPipe, TimeDurationPipe, TimeAgoPipe, TranslocoModule]
}) })
export class UserStatsInfoCardsComponent { export class UserStatsInfoCardsComponent {
@ -27,7 +27,7 @@ export class UserStatsInfoCardsComponent {
@Input() lastActive: string = ''; @Input() lastActive: string = '';
@Input() avgHoursPerWeekSpentReading: number = 0; @Input() avgHoursPerWeekSpentReading: number = 0;
constructor(private statsService: StatisticsService, private modalService: NgbModal) { } constructor(private statsService: StatisticsService, private modalService: NgbModal, private accountService: AccountService) { }
openPageByYearList() { openPageByYearList() {
const numberPipe = new CompactNumberPipe(); const numberPipe = new CompactNumberPipe();
@ -46,5 +46,4 @@ export class UserStatsInfoCardsComponent {
ref.componentInstance.title = 'Words Read By Year'; ref.componentInstance.title = 'Words Read By Year';
}); });
} }
} }

View File

@ -31,7 +31,7 @@
"user-scrobble-history": { "user-scrobble-history": {
"title": "Scrobble History", "title": "Scrobble History",
"description": "Here you will find any scrobble events linked with your account. In order for events to exist, you must have an active\n scrobble provider configured. All events that have been processed will clear after a month. If there are non-processed events, it\n is likely these cannot form matches upstream. Please reach out to your admin to get them corrected.", "description": "Here you will find any scrobble events linked with your account. In order for events to exist, you must have an active scrobble provider configured. All events that have been processed will clear after a month. If there are non-processed events, it is likely these cannot form matches upstream. Please reach out to your admin to get them corrected.",
"filter-label": "Filter", "filter-label": "Filter",
"created-header": "Created", "created-header": "Created",
"last-modified-header": "Last Modified", "last-modified-header": "Last Modified",
@ -164,7 +164,7 @@
"user-holds": { "user-holds": {
"title": "Scrobble Holds", "title": "Scrobble Holds",
"description": "This is a user-managed list of Series that will not be scrobbled to upstream providers. You can remove a series at\n any time and the next Scrobble-able event (reading progress, rating, want to read status) will trigger events." "description": "This is a user-managed list of Series that will not be scrobbled to upstream providers. You can remove a series at any time and the next Scrobble-able event (reading progress, rating, want to read status) will trigger events."
}, },
"theme-manager": { "theme-manager": {
@ -243,7 +243,7 @@
"current-password-label": "Current Password", "current-password-label": "Current Password",
"email-not-confirmed": "This email is not confirmed", "email-not-confirmed": "This email is not confirmed",
"email-updated-title": "Email Updated", "email-updated-title": "Email Updated",
"email-updated-description": "You can use the following link below to confirm the email for your account.\n If your server is externally accessible, an email will have been sent to the email and the link can be used to confirm the email.", "email-updated-description": "You can use the following link below to confirm the email for your account. If your server is externally accessible, an email will have been sent to the email and the link can be used to confirm the email.",
"setup-user-account": "Setup user's account", "setup-user-account": "Setup user's account",
"invite-url-label": "Invite Url", "invite-url-label": "Invite Url",
"invite-url-tooltip": "Copy this and paste in a new tab", "invite-url-tooltip": "Copy this and paste in a new tab",
@ -280,7 +280,7 @@
"no-token-set": "No Token Set", "no-token-set": "No Token Set",
"token-set": "Token Set", "token-set": "Token Set",
"generate": "Generate", "generate": "Generate",
"instructions": "First time users should click on \"{{scrobbling-providers.generate}}\" below to allow Kavita+ to talk with {{service}}.\n Once you authorize the program, copy and paste the token in the input below. You can regenerate your token at any time.", "instructions": "First time users should click on \"{{scrobbling-providers.generate}}\" below to allow Kavita+ to talk with {{service}}. Once you authorize the program, copy and paste the token in the input below. You can regenerate your token at any time.",
"token-input-label": "{{service}} Token Goes Here", "token-input-label": "{{service}} Token Goes Here",
"edit": "{{common.edit}}", "edit": "{{common.edit}}",
"cancel": "{{common.cancel}}", "cancel": "{{common.cancel}}",
@ -540,11 +540,11 @@
"invite-user": { "invite-user": {
"title": "Invite User", "title": "Invite User",
"close": "{{common.close}}", "close": "{{common.close}}",
"description": "Invite a user to your server. Enter their email in and we will send them an email to create an account. If you do not want to use our email service, you can <a href=\"https://wiki.kavitareader.com/en/guides/misc/email\" rel=\"noopener noreferrer\" target=\"_blank\">host your own</a>\n email service or use a fake email (Forgot User will not work). A link will be presented regardless and can be used to setup the account manually.", "description": "Invite a user to your server. Enter their email in and we will send them an email to create an account. If you do not want to use our email service, you can <a href=\"https://wiki.kavitareader.com/en/guides/misc/email\" rel=\"noopener noreferrer\" target=\"_blank\">host your own</a> email service or use a fake email (Forgot User will not work). A link will be presented regardless and can be used to setup the account manually.",
"email": "{{common.email}}", "email": "{{common.email}}",
"required-field": "{{common.required-field}}", "required-field": "{{common.required-field}}",
"setup-user-title": "User invited", "setup-user-title": "User invited",
"setup-user-description": "You can use the following link below to setup the account for your user or use the copy button. You may need to log out before using the link to register a new user.\n If your server is externally accessible, an email will have been sent to the user and the links can be used by them to finish setting up their account.", "setup-user-description": "You can use the following link below to setup the account for your user or use the copy button. You may need to log out before using the link to register a new user. If your server is externally accessible, an email will have been sent to the user and the links can be used by them to finish setting up their account.",
"setup-user-account": "Setup user's account", "setup-user-account": "Setup user's account",
"setup-user-account-tooltip": "Copy this and paste in a new tab. You may need to log out.", "setup-user-account-tooltip": "Copy this and paste in a new tab. You may need to log out.",
"invite-url-label": "Invite Url", "invite-url-label": "Invite Url",
@ -659,7 +659,7 @@
"description": "Complete the form to register an admin account", "description": "Complete the form to register an admin account",
"username-label": "{{common.username}}", "username-label": "{{common.username}}",
"email-label": "{{common.email}}", "email-label": "{{common.email}}",
"email-tooltip": "Email does not need to be a real address, but provides access to forgot password.\n It is not sent outside the server unless forgot password is used without a custom email service host.", "email-tooltip": "Email does not need to be a real address, but provides access to forgot password. It is not sent outside the server unless forgot password is used without a custom email service host.",
"password-label": "{{common.password}}", "password-label": "{{common.password}}",
"required-field": "{{validation.required-field}}", "required-field": "{{validation.required-field}}",
"valid-email": "{{validation.valid-email}}", "valid-email": "{{validation.valid-email}}",
@ -941,6 +941,7 @@
"length-title": "Length", "length-title": "Length",
"pages-count": "{{num}} Pages", "pages-count": "{{num}} Pages",
"words-count": "{{num}} Words", "words-count": "{{num}} Words",
"reading-time-title": "Read Time", "reading-time-title": "Read Time",
"date-added-title": "Date Added", "date-added-title": "Date Added",
"size-title": "Size", "size-title": "Size",
@ -952,7 +953,7 @@
"range-hours": "{{value}} {{hourWord}}", "range-hours": "{{value}} {{hourWord}}",
"hour": "Hour", "hour": "Hour",
"hours": "Hours", "hours": "Hours",
"read-time-title": "{{series-info-cards.read-time-title}}" "read-time-title": "{{series-info-cards.read-time-title}}",
}, },
"series-info-cards": { "series-info-cards": {
@ -1069,7 +1070,7 @@
}, },
"manage-scrobble-errors": { "manage-scrobble-errors": {
"description": "This table contains issues found during scrobbling. This list is non-managed.\n You can clear it at any time and wait for the next scrobble upload to see. If there is an unknown series, you are best correcting the\nseries name or localized series name or adding a weblink for the providers.", "description": "This table contains issues found during scrobbling. This list is non-managed. You can clear it at any time and wait for the next scrobble upload to see. If there is an unknown series, you are best correcting the series name or localized series name or adding a weblink for the providers.",
"filter-label": "Filter", "filter-label": "Filter",
"clear-errors": "Clear Errors", "clear-errors": "Clear Errors",
"series-header": "Series", "series-header": "Series",
@ -1614,7 +1615,13 @@
"x-axis-label": "Time", "x-axis-label": "Time",
"y-axis-label": "Hours Read", "y-axis-label": "Hours Read",
"no-data": "No Reading Progress", "no-data": "No Reading Progress",
"time-frame-label": "Time Frame" "time-frame-label": "Time Frame",
"this-week": "{{time-periods.this-week}}",
"last-7-days": "{{time-periods.last-7-days}}",
"last-30-days": "{{time-periods.last-30-days}}",
"last-90-days": "{{time-periods.last-90-days}}",
"last-year": "{{time-periods.last-year}}",
"all-time": "{{time-periods.all-time}}"
}, },
"manga-format-stats": { "manga-format-stats": {
@ -1753,7 +1760,7 @@
"confirm-delete-series": "Are you sure you want to delete this series? It will not modify files on disk.", "confirm-delete-series": "Are you sure you want to delete this series? It will not modify files on disk.",
"alert-bad-theme": "There is invalid or unsafe css in the theme. Please reach out to your admin to have this corrected. Defaulting to dark theme.", "alert-bad-theme": "There is invalid or unsafe css in the theme. Please reach out to your admin to have this corrected. Defaulting to dark theme.",
"confirm-library-delete": "Are you sure you want to delete the {{name}} library? You cannot undo this action.", "confirm-library-delete": "Are you sure you want to delete the {{name}} library? You cannot undo this action.",
"confirm-library-type-change": "Changing library type will trigger a new scan with different parsing rules and may lead to\n series being re-created and hence you may loose progress and bookmarks. You should backup before you do this. Are you sure you want to continue?", "confirm-library-type-change": "Changing library type will trigger a new scan with different parsing rules and may lead to series being re-created and hence you may loose progress and bookmarks. You should backup before you do this. Are you sure you want to continue?",
"confirm-download-size": "The {{entityType}} is {{size}}. Are you sure you want to continue?" "confirm-download-size": "The {{entityType}} is {{size}}. Are you sure you want to continue?"
}, },

View File

@ -33,7 +33,7 @@ export function preloadUser(userService: AccountService, transloco: TranslocoSer
} }
// If no user or locale is available, fallback to the default language ('en') // If no user or locale is available, fallback to the default language ('en')
const localStorageLocale = localStorage.getItem(userService.localeKey) || 'en'; const localStorageLocale = localStorage.getItem(AccountService.localeKey) || 'en';
transloco.setActiveLang(localStorageLocale); transloco.setActiveLang(localStorageLocale);
return transloco.load(localStorageLocale) return transloco.load(localStorageLocale)
})).subscribe(); })).subscribe();
@ -71,7 +71,7 @@ const languageCodes = [
'syr', 'syr-SY', 'ta', 'ta-IN', 'te', 'te-IN', 'th', 'th-TH', 'tl', 'tl-PH', 'tn', 'syr', 'syr-SY', 'ta', 'ta-IN', 'te', 'te-IN', 'th', 'th-TH', 'tl', 'tl-PH', 'tn',
'tn-ZA', 'tr', 'tr-TR', 'tt', 'tt-RU', 'ts', 'uk', 'uk-UA', 'ur', 'ur-PK', 'uz', 'tn-ZA', 'tr', 'tr-TR', 'tt', 'tt-RU', 'ts', 'uk', 'uk-UA', 'ur', 'ur-PK', 'uz',
'uz-UZ', 'uz-UZ', 'vi', 'vi-VN', 'xh', 'xh-ZA', 'zh', 'zh-CN', 'zh-HK', 'zh-MO', 'uz-UZ', 'uz-UZ', 'vi', 'vi-VN', 'xh', 'xh-ZA', 'zh', 'zh-CN', 'zh-HK', 'zh-MO',
'zh-SG', 'zh-TW', 'zu', 'zu-ZA' 'zh-SG', 'zh-TW', 'zu', 'zu-ZA', 'zh_Hans'
]; ];
bootstrapApplication(AppComponent, { bootstrapApplication(AppComponent, {
@ -109,13 +109,13 @@ bootstrapApplication(AppComponent, {
provide: TRANSLOCO_CONFIG, provide: TRANSLOCO_CONFIG,
useValue: { useValue: {
reRenderOnLangChange: true, reRenderOnLangChange: true,
availableLangs: languageCodes, // TODO: Derive this from the directory availableLangs: languageCodes,
prodMode: environment.production, prodMode: environment.production,
defaultLang: 'en', defaultLang: 'en',
fallbackLang: 'en', fallbackLang: 'en',
missingHandler: { missingHandler: {
useFallbackTranslation: true, useFallbackTranslation: true,
allowEmpty: true, allowEmpty: false,
}, },
flatten: { flatten: {
aot: !isDevMode() aot: !isDevMode()

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0", "name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
}, },
"version": "0.7.6.5" "version": "0.7.6.6"
}, },
"servers": [ "servers": [
{ {
@ -12822,16 +12822,6 @@
"type": "integer", "type": "integer",
"description": "Platform (ie) Windows 10", "description": "Platform (ie) Windows 10",
"format": "int32" "format": "int32"
},
"lastUsed": {
"type": "string",
"description": "Last time this device was used to send a file",
"format": "date-time"
},
"lastUsedUtc": {
"type": "string",
"description": "Last time this device was used to send a file",
"format": "date-time"
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@ -13391,18 +13381,6 @@
"description": "Human Readable title for the Job", "description": "Human Readable title for the Job",
"nullable": true "nullable": true
}, },
"createdAt": {
"type": "string",
"description": "When the job was created",
"format": "date-time",
"nullable": true
},
"lastExecution": {
"type": "string",
"description": "Last time the job was run",
"format": "date-time",
"nullable": true
},
"createdAtUtc": { "createdAtUtc": {
"type": "string", "type": "string",
"description": "When the job was created", "description": "When the job was created",
@ -13879,14 +13857,6 @@
"type": "string", "type": "string",
"description": "Exception message", "description": "Exception message",
"nullable": true "nullable": true
},
"created": {
"type": "string",
"format": "date-time"
},
"createdUtc": {
"type": "string",
"format": "date-time"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -14850,11 +14820,11 @@
"format": "int32", "format": "int32",
"nullable": true "nullable": true
}, },
"lastModified": { "lastModifiedUtc": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
}, },
"created": { "createdUtc": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
}, },
@ -16439,22 +16409,6 @@
"description": "Where did the theme come from", "description": "Where did the theme come from",
"format": "int32" "format": "int32"
}, },
"created": {
"type": "string",
"format": "date-time"
},
"lastModified": {
"type": "string",
"format": "date-time"
},
"createdUtc": {
"type": "string",
"format": "date-time"
},
"lastModifiedUtc": {
"type": "string",
"format": "date-time"
},
"selector": { "selector": {
"type": "string", "type": "string",
"nullable": true, "nullable": true,