diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index b29823f53e..7c02076191 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -4,6 +4,7 @@ import { DomSanitizer } from "@angular/platform-browser"; import { NavigationEnd, Router } from "@angular/router"; import * as jq from "jquery"; import { IndividualConfig, ToastrService } from "ngx-toastr"; +import { Subject, takeUntil } from "rxjs"; import Swal from "sweetalert2"; import { AuthService } from "@bitwarden/common/abstractions/auth.service"; @@ -48,6 +49,7 @@ export class AppComponent implements OnDestroy, OnInit { private lastActivity: number = null; private idleTimer: number = null; private isIdle = false; + private destroy$ = new Subject(); constructor( @Inject(DOCUMENT) private document: Document, @@ -78,7 +80,9 @@ export class AppComponent implements OnDestroy, OnInit { ) {} ngOnInit() { - this.document.documentElement.lang = this.i18nService.locale; + this.i18nService.locale$.pipe(takeUntil(this.destroy$)).subscribe((locale) => { + this.document.documentElement.lang = locale; + }); this.ngZone.runOutsideAngular(() => { window.onmousemove = () => this.recordActivity(); @@ -181,7 +185,7 @@ export class AppComponent implements OnDestroy, OnInit { }); }); - this.router.events.subscribe((event) => { + this.router.events.pipe(takeUntil(this.destroy$)).subscribe((event) => { if (event instanceof NavigationEnd) { const modals = Array.from(document.querySelectorAll(".modal")); for (const modal of modals) { @@ -211,6 +215,8 @@ export class AppComponent implements OnDestroy, OnInit { ngOnDestroy() { this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + this.destroy$.next(); + this.destroy$.unsubscribe(); } private async logOut(expired: boolean) { diff --git a/apps/web/src/app/settings/sponsoring-org-row.component.ts b/apps/web/src/app/settings/sponsoring-org-row.component.ts index 4dabff04b6..db898e5cba 100644 --- a/apps/web/src/app/settings/sponsoring-org-row.component.ts +++ b/apps/web/src/app/settings/sponsoring-org-row.component.ts @@ -1,5 +1,6 @@ import { formatDate } from "@angular/common"; import { Component, EventEmitter, Input, Output, OnInit } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; @@ -23,6 +24,8 @@ export class SponsoringOrgRowComponent implements OnInit { revokeSponsorshipPromise: Promise; resendEmailPromise: Promise; + private locale = ""; + constructor( private apiService: ApiService, private i18nService: I18nService, @@ -30,7 +33,9 @@ export class SponsoringOrgRowComponent implements OnInit { private platformUtilsService: PlatformUtilsService ) {} - ngOnInit(): void { + async ngOnInit() { + this.locale = await firstValueFrom(this.i18nService.locale$); + this.setStatus( this.isSelfHosted, this.sponsoringOrg.familySponsorshipToDelete, @@ -98,7 +103,7 @@ export class SponsoringOrgRowComponent implements OnInit { // They want to delete but there is a valid until date which means there is an active sponsorship this.statusMessage = this.i18nService.t( "revokeWhenExpired", - formatDate(validUntil, "MM/dd/yyyy", this.i18nService.locale) + formatDate(validUntil, "MM/dd/yyyy", this.locale) ); this.statusClass = "text-danger"; } else if (toDelete) { diff --git a/libs/common/src/abstractions/i18n.service.ts b/libs/common/src/abstractions/i18n.service.ts index 4706e0d1aa..23f2d4fd23 100644 --- a/libs/common/src/abstractions/i18n.service.ts +++ b/libs/common/src/abstractions/i18n.service.ts @@ -1,5 +1,7 @@ +import { Observable } from "rxjs"; + export abstract class I18nService { - locale: string; + locale$: Observable; supportedTranslationLocales: string[]; translationLocale: string; collator: Intl.Collator; diff --git a/libs/common/src/services/i18n.service.ts b/libs/common/src/services/i18n.service.ts index fda8f839ea..9281cc8cf4 100644 --- a/libs/common/src/services/i18n.service.ts +++ b/libs/common/src/services/i18n.service.ts @@ -1,7 +1,10 @@ +import { Observable, ReplaySubject } from "rxjs"; + import { I18nService as I18nServiceAbstraction } from "../abstractions/i18n.service"; export class I18nService implements I18nServiceAbstraction { - locale: string; + private _locale = new ReplaySubject(1); + locale$: Observable = this._locale.asObservable(); // First locale is the default (English) supportedTranslationLocales: string[] = ["en"]; translationLocale: string; @@ -85,10 +88,14 @@ export class I18nService implements I18nServiceAbstraction { } this.inited = true; - this.locale = this.translationLocale = locale != null ? locale : this.systemLanguage; + this.translationLocale = locale != null ? locale : this.systemLanguage; + this._locale.next(this.translationLocale); try { - this.collator = new Intl.Collator(this.locale, { numeric: true, sensitivity: "base" }); + this.collator = new Intl.Collator(this.translationLocale, { + numeric: true, + sensitivity: "base", + }); } catch { this.collator = null; } diff --git a/libs/common/src/services/search.service.ts b/libs/common/src/services/search.service.ts index c020dd299f..fe2c87c33d 100644 --- a/libs/common/src/services/search.service.ts +++ b/libs/common/src/services/search.service.ts @@ -14,16 +14,23 @@ export class SearchService implements SearchServiceAbstraction { indexedEntityId?: string = null; private indexing = false; private index: lunr.Index = null; - private searchableMinLength = 2; + private readonly immediateSearchLocales: string[] = ["zh-CN", "zh-TW", "ja", "ko", "vi"]; + private readonly defaultSearchableMinLength: number = 2; + private searchableMinLength: number = this.defaultSearchableMinLength; constructor( private cipherService: CipherService, private logService: LogService, private i18nService: I18nService ) { - if (["zh-CN", "zh-TW"].indexOf(i18nService.locale) !== -1) { - this.searchableMinLength = 1; - } + this.i18nService.locale$.subscribe((locale) => { + if (this.immediateSearchLocales.indexOf(locale) !== -1) { + this.searchableMinLength = 1; + } else { + this.searchableMinLength = this.defaultSearchableMinLength; + } + }); + //register lunr pipeline function lunr.Pipeline.registerFunction(this.normalizeAccentsPipelineFunction, "normalizeAccents"); } diff --git a/libs/components/src/utils/i18n-mock.service.ts b/libs/components/src/utils/i18n-mock.service.ts index ffc2b031d3..19f756fc36 100644 --- a/libs/components/src/utils/i18n-mock.service.ts +++ b/libs/components/src/utils/i18n-mock.service.ts @@ -1,7 +1,9 @@ +import { Observable } from "rxjs"; + import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; export class I18nMockService implements I18nService { - locale: string; + locale$: Observable; supportedTranslationLocales: string[]; translationLocale: string; collator: Intl.Collator;