Version Fix and Locale Updates (#3626)

This commit is contained in:
Joe Milazzo 2025-03-13 17:54:55 -05:00 committed by GitHub
parent b644022f30
commit a6ccae5849
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 360 additions and 58 deletions

View file

@ -3,3 +3,10 @@ export interface Language {
title: string;
}
export interface KavitaLocale {
fileName: string; // isoCode aka what maps to the file on disk and what transloco loads
renderName: string;
translationCompletion: number;
isRtL: boolean;
hash: string;
}

View file

@ -18,6 +18,7 @@ import {Action} from "./action-factory.service";
import {CoverImageSize} from "../admin/_models/cover-image-size";
import {LicenseInfo} from "../_models/kavitaplus/license-info";
import {LicenseService} from "./license.service";
import {LocalizationService} from "./localization.service";
export enum Role {
Admin = 'Admin',
@ -48,6 +49,7 @@ export class AccountService {
private readonly destroyRef = inject(DestroyRef);
private readonly licenseService = inject(LicenseService);
private readonly localizationService = inject(LocalizationService);
baseUrl = environment.apiUrl;
userKey = 'kavita-user';
@ -168,6 +170,8 @@ export class AccountService {
}
setCurrentUser(user?: User, refreshConnections = true) {
const isSameUser = this.currentUser === user;
if (user) {
user.roles = [];
const roles = this.getDecodedToken(user.token).role;
@ -197,7 +201,9 @@ export class AccountService {
// But that really messes everything up
this.messageHub.stopHubConnection();
this.messageHub.createHubConnection(this.currentUser);
this.licenseService.hasValidLicense().subscribe();
if (!isSameUser) {
this.licenseService.hasValidLicense().subscribe();
}
this.startRefreshTokenTimer();
}
}
@ -316,6 +322,8 @@ export class AccountService {
// Update the locale on disk (for logout and compact-number pipe)
localStorage.setItem(AccountService.localeKey, this.currentUser.preferences.locale);
this.localizationService.refreshTranslations(this.currentUser.preferences.locale);
}
return settings;
}), takeUntilDestroyed(this.destroyRef));

View file

@ -1,18 +1,36 @@
import { Injectable } from '@angular/core';
import {inject, Injectable} from '@angular/core';
import {environment} from "../../environments/environment";
import { HttpClient } from "@angular/common/http";
import {Language} from "../_models/metadata/language";
import {KavitaLocale, Language} from "../_models/metadata/language";
import {ReplaySubject, tap} from "rxjs";
import {TranslocoService} from "@jsverse/transloco";
@Injectable({
providedIn: 'root'
})
export class LocalizationService {
private readonly translocoService = inject(TranslocoService);
baseUrl = environment.apiUrl;
private readonly localeSubject = new ReplaySubject<KavitaLocale[]>(1);
public readonly locales$ = this.localeSubject.asObservable();
constructor(private httpClient: HttpClient) { }
getLocales() {
return this.httpClient.get<Language[]>(this.baseUrl + 'locale');
return this.httpClient.get<KavitaLocale[]>(this.baseUrl + 'locale').pipe(tap(locales => {
this.localeSubject.next(locales);
}));
}
refreshTranslations(lang: string) {
// Clear the cached translation
localStorage.removeItem(`@@TRANSLOCO_PERSIST_TRANSLATIONS/${lang}`);
// Reload the translation
return this.translocoService.load(lang);
}
}

View file

@ -19,13 +19,12 @@ import {SideNavComponent} from './sidenav/_components/side-nav/side-nav.componen
import {NavHeaderComponent} from "./nav/_components/nav-header/nav-header.component";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {ServerService} from "./_services/server.service";
import {OutOfDateModalComponent} from "./announcements/_components/out-of-date-modal/out-of-date-modal.component";
import {PreferenceNavComponent} from "./sidenav/preference-nav/preference-nav.component";
import {Breakpoint, UtilityService} from "./shared/_services/utility.service";
import {TranslocoService} from "@jsverse/transloco";
import {User} from "./_models/user";
import {VersionService} from "./_services/version.service";
import {LicenseService} from "./_services/license.service";
import {LocalizationService} from "./_services/localization.service";
@Component({
selector: 'app-root',
@ -36,8 +35,9 @@ import {LicenseService} from "./_services/license.service";
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent implements OnInit {
protected readonly Breakpoint = Breakpoint;
transitionState$!: Observable<boolean>;
private readonly destroyRef = inject(DestroyRef);
private readonly offcanvas = inject(NgbOffcanvas);
@ -53,8 +53,9 @@ export class AppComponent implements OnInit {
private readonly translocoService = inject(TranslocoService);
private readonly versionService = inject(VersionService); // Needs to be injected to run background job
private readonly licenseService = inject(LicenseService);
private readonly localizationService = inject(LocalizationService);
protected readonly Breakpoint = Breakpoint;
transitionState$!: Observable<boolean>;
constructor(ratingConfig: NgbRatingConfig, modalConfig: NgbModalConfig) {
@ -112,6 +113,7 @@ export class AppComponent implements OnInit {
this.setDocHeight();
this.setCurrentUser();
this.themeService.setColorScape('');
this.localizationService.getLocales().subscribe(); // This will cache the localizations on startup
}

View file

@ -15,8 +15,8 @@
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="global-header" formControlName="locale">
@for(opt of locales; track opt.title) {
<option [value]="opt.isoCode">{{opt.title | titlecase}}</option>
@for(opt of locales; track opt.renderName) {
<option [value]="opt.fileName">{{opt.renderName | titlecase}} ({{opt.translationCompletion | number:'1.0-1'}}%)</option>
}
</select>
</ng-template>

View file

@ -21,7 +21,7 @@ import {LocalizationService} from "../../_services/localization.service";
import {bookColorThemes} from "../../book-reader/_components/reader-settings/reader-settings.component";
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
import {User} from "../../_models/user";
import {Language} from "../../_models/metadata/language";
import {KavitaLocale, Language} from "../../_models/metadata/language";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {debounceTime, distinctUntilChanged, filter, forkJoin, switchMap, tap} from "rxjs";
import {take} from "rxjs/operators";
@ -35,7 +35,7 @@ import {
NgbAccordionDirective, NgbAccordionHeader,
NgbAccordionItem, NgbTooltip
} from "@ng-bootstrap/ng-bootstrap";
import {AsyncPipe, NgStyle, NgTemplateOutlet, TitleCasePipe} from "@angular/common";
import {AsyncPipe, DecimalPipe, NgStyle, NgTemplateOutlet, TitleCasePipe} from "@angular/common";
import {ColorPickerModule} from "ngx-color-picker";
import {SettingTitleComponent} from "../../settings/_components/setting-title/setting-title.component";
import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component";
@ -76,7 +76,8 @@ import {LicenseService} from "../../_services/license.service";
PdfSpreadModePipe,
PdfThemePipe,
PdfScrollModePipe,
AsyncPipe
AsyncPipe,
DecimalPipe
],
templateUrl: './manage-user-preferences.component.html',
styleUrl: './manage-user-preferences.component.scss',
@ -112,7 +113,7 @@ export class ManageUserPreferencesComponent implements OnInit {
fontFamilies: Array<string> = [];
locales: Array<Language> = [{title: 'English', isoCode: 'en'}];
locales: Array<KavitaLocale> = [];
settingsForm: FormGroup = new FormGroup({});
user: User | undefined = undefined;
@ -120,7 +121,7 @@ export class ManageUserPreferencesComponent implements OnInit {
get Locale() {
if (!this.settingsForm.get('locale')) return 'English';
return this.locales.filter(l => l.isoCode === this.settingsForm.get('locale')!.value)[0].title;
return this.locales.filter(l => l.fileName === this.settingsForm.get('locale')!.value)[0].renderName;
}
@ -128,7 +129,7 @@ export class ManageUserPreferencesComponent implements OnInit {
this.fontFamilies = this.bookService.getFontFamilies().map(f => f.title);
this.cdRef.markForCheck();
this.localizationService.getLocales().subscribe(res => {
this.localizationService.locales$.subscribe(res => {
this.locales = res;
this.cdRef.markForCheck();

View file

@ -30,6 +30,7 @@ import {LazyLoadImageModule} from "ng-lazyload-image";
import {getSaver, SAVER} from "./app/_providers/saver.provider";
import {distinctUntilChanged} from "rxjs/operators";
import {APP_BASE_HREF, PlatformLocation} from "@angular/common";
import {provideTranslocoDefaultLocale} from "@jsverse/transloco-locale/lib/transloco-locale.providers";
const disableAnimations = !('animate' in document.documentElement);
@ -112,7 +113,9 @@ const translocoOptions = {
missingHandler: {
useFallbackTranslation: true,
allowEmpty: false,
logMissingKey: true
},
failedRetries: 2,
} as TranslocoConfig
};