mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +00:00
Add lang attr on desktop and browser (#14691)
This commit is contained in:
@@ -1,11 +1,20 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, inject } from "@angular/core";
|
import {
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
DestroyRef,
|
||||||
|
NgZone,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
inject,
|
||||||
|
} from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
|
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
|
||||||
import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap } from "rxjs";
|
import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap } from "rxjs";
|
||||||
|
|
||||||
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
||||||
|
import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n";
|
||||||
import { LogoutReason } from "@bitwarden/auth/common";
|
import { LogoutReason } from "@bitwarden/auth/common";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
@@ -73,9 +82,14 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
private biometricStateService: BiometricStateService,
|
private biometricStateService: BiometricStateService,
|
||||||
private biometricsService: BiometricsService,
|
private biometricsService: BiometricsService,
|
||||||
private deviceTrustToastService: DeviceTrustToastService,
|
private deviceTrustToastService: DeviceTrustToastService,
|
||||||
|
private readonly destoryRef: DestroyRef,
|
||||||
|
private readonly documentLangSetter: DocumentLangSetter,
|
||||||
private popupSizeService: PopupSizeService,
|
private popupSizeService: PopupSizeService,
|
||||||
) {
|
) {
|
||||||
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
||||||
|
|
||||||
|
const langSubscription = this.documentLangSetter.start();
|
||||||
|
this.destoryRef.onDestroy(() => langSubscription.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
|
DestroyRef,
|
||||||
NgZone,
|
NgZone,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
@@ -25,6 +26,7 @@ import {
|
|||||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||||
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
||||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||||
|
import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { FingerprintDialogComponent, LoginApprovalComponent } from "@bitwarden/auth/angular";
|
import { FingerprintDialogComponent, LoginApprovalComponent } from "@bitwarden/auth/angular";
|
||||||
import { DESKTOP_SSO_CALLBACK, LogoutReason } from "@bitwarden/auth/common";
|
import { DESKTOP_SSO_CALLBACK, LogoutReason } from "@bitwarden/auth/common";
|
||||||
@@ -163,8 +165,13 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private deviceTrustToastService: DeviceTrustToastService,
|
private deviceTrustToastService: DeviceTrustToastService,
|
||||||
|
private readonly destroyRef: DestroyRef,
|
||||||
|
private readonly documentLangSetter: DocumentLangSetter,
|
||||||
) {
|
) {
|
||||||
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
||||||
|
|
||||||
|
const langSubscription = this.documentLangSetter.start();
|
||||||
|
this.destroyRef.onDestroy(() => langSubscription.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { DOCUMENT } from "@angular/common";
|
import { Component, DestroyRef, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
|
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { Subject, filter, firstValueFrom, map, takeUntil, timeout } from "rxjs";
|
import { Subject, filter, firstValueFrom, map, timeout } from "rxjs";
|
||||||
|
|
||||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||||
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
||||||
|
import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n";
|
||||||
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
@@ -60,7 +60,6 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
loading = false;
|
loading = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DOCUMENT) private document: Document,
|
|
||||||
private broadcasterService: BroadcasterService,
|
private broadcasterService: BroadcasterService,
|
||||||
private folderService: InternalFolderService,
|
private folderService: InternalFolderService,
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
@@ -86,15 +85,16 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private processReloadService: ProcessReloadServiceAbstraction,
|
private processReloadService: ProcessReloadServiceAbstraction,
|
||||||
private deviceTrustToastService: DeviceTrustToastService,
|
private deviceTrustToastService: DeviceTrustToastService,
|
||||||
|
private readonly destoryRef: DestroyRef,
|
||||||
|
private readonly documentLangSetter: DocumentLangSetter,
|
||||||
) {
|
) {
|
||||||
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
||||||
|
|
||||||
|
const langSubscription = this.documentLangSetter.start();
|
||||||
|
this.destoryRef.onDestroy(() => langSubscription.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.i18nService.locale$.pipe(takeUntil(this.destroy$)).subscribe((locale) => {
|
|
||||||
this.document.documentElement.lang = locale;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ngZone.runOutsideAngular(() => {
|
this.ngZone.runOutsideAngular(() => {
|
||||||
window.onmousemove = () => this.recordActivity();
|
window.onmousemove = () => this.recordActivity();
|
||||||
window.onmousedown = () => this.recordActivity();
|
window.onmousedown = () => this.recordActivity();
|
||||||
|
|||||||
36
libs/angular/src/platform/i18n/document-lang.setter.spec.ts
Normal file
36
libs/angular/src/platform/i18n/document-lang.setter.spec.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { Subject } from "rxjs";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
|
||||||
|
import { DocumentLangSetter } from "./document-lang.setter";
|
||||||
|
|
||||||
|
describe("DocumentLangSetter", () => {
|
||||||
|
const document = mock<Document>();
|
||||||
|
const i18nService = mock<I18nService>();
|
||||||
|
|
||||||
|
const sut = new DocumentLangSetter(document, i18nService);
|
||||||
|
|
||||||
|
describe("start", () => {
|
||||||
|
it("reacts to locale changes while start called with a non-closed subscription", async () => {
|
||||||
|
const localeSubject = new Subject<string>();
|
||||||
|
i18nService.locale$ = localeSubject;
|
||||||
|
|
||||||
|
localeSubject.next("en");
|
||||||
|
|
||||||
|
expect(document.documentElement.lang).toBeFalsy();
|
||||||
|
|
||||||
|
const sub = sut.start();
|
||||||
|
|
||||||
|
localeSubject.next("es");
|
||||||
|
|
||||||
|
expect(document.documentElement.lang).toBe("es");
|
||||||
|
|
||||||
|
sub.unsubscribe();
|
||||||
|
|
||||||
|
localeSubject.next("ar");
|
||||||
|
|
||||||
|
expect(document.documentElement.lang).toBe("es");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
26
libs/angular/src/platform/i18n/document-lang.setter.ts
Normal file
26
libs/angular/src/platform/i18n/document-lang.setter.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Subscription } from "rxjs";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service for managing the setting of the `lang="<locale>" attribute on the
|
||||||
|
* main document for the application.
|
||||||
|
*/
|
||||||
|
export class DocumentLangSetter {
|
||||||
|
constructor(
|
||||||
|
private readonly document: Document,
|
||||||
|
private readonly i18nService: I18nService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts listening to an upstream source for the best locale for the user
|
||||||
|
* and applies it to the application document.
|
||||||
|
* @returns A subscription that can be unsubscribed if you wish to stop
|
||||||
|
* applying lang attribute updates to the application document.
|
||||||
|
*/
|
||||||
|
start(): Subscription {
|
||||||
|
return this.i18nService.locale$.subscribe((locale) => {
|
||||||
|
this.document.documentElement.lang = locale;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
1
libs/angular/src/platform/i18n/index.ts
Normal file
1
libs/angular/src/platform/i18n/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { DocumentLangSetter } from "./document-lang.setter";
|
||||||
@@ -21,6 +21,7 @@ import { SafeInjectionToken } from "@bitwarden/ui-common";
|
|||||||
export { SafeInjectionToken } from "@bitwarden/ui-common";
|
export { SafeInjectionToken } from "@bitwarden/ui-common";
|
||||||
|
|
||||||
export const WINDOW = new SafeInjectionToken<Window>("WINDOW");
|
export const WINDOW = new SafeInjectionToken<Window>("WINDOW");
|
||||||
|
export const DOCUMENT = new SafeInjectionToken<Document>("DOCUMENT");
|
||||||
export const OBSERVABLE_MEMORY_STORAGE = new SafeInjectionToken<
|
export const OBSERVABLE_MEMORY_STORAGE = new SafeInjectionToken<
|
||||||
AbstractStorageService & ObservableStorageService
|
AbstractStorageService & ObservableStorageService
|
||||||
>("OBSERVABLE_MEMORY_STORAGE");
|
>("OBSERVABLE_MEMORY_STORAGE");
|
||||||
|
|||||||
@@ -337,6 +337,7 @@ import {
|
|||||||
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction";
|
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction";
|
||||||
import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation";
|
import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation";
|
||||||
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
|
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
|
||||||
|
import { DocumentLangSetter } from "../platform/i18n";
|
||||||
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
|
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
|
||||||
import { LoggingErrorHandler } from "../platform/services/logging-error-handler";
|
import { LoggingErrorHandler } from "../platform/services/logging-error-handler";
|
||||||
import { AngularThemingService } from "../platform/services/theming/angular-theming.service";
|
import { AngularThemingService } from "../platform/services/theming/angular-theming.service";
|
||||||
@@ -349,6 +350,7 @@ import { NoopViewCacheService } from "../platform/view-cache/internal";
|
|||||||
import {
|
import {
|
||||||
CLIENT_TYPE,
|
CLIENT_TYPE,
|
||||||
DEFAULT_VAULT_TIMEOUT,
|
DEFAULT_VAULT_TIMEOUT,
|
||||||
|
DOCUMENT,
|
||||||
ENV_ADDITIONAL_REGIONS,
|
ENV_ADDITIONAL_REGIONS,
|
||||||
HTTP_OPERATIONS,
|
HTTP_OPERATIONS,
|
||||||
INTRAPROCESS_MESSAGING_SUBJECT,
|
INTRAPROCESS_MESSAGING_SUBJECT,
|
||||||
@@ -378,6 +380,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider(ModalService),
|
safeProvider(ModalService),
|
||||||
safeProvider(PasswordRepromptService),
|
safeProvider(PasswordRepromptService),
|
||||||
safeProvider({ provide: WINDOW, useValue: window }),
|
safeProvider({ provide: WINDOW, useValue: window }),
|
||||||
|
safeProvider({ provide: DOCUMENT, useValue: document }),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: LOCALE_ID as SafeInjectionToken<string>,
|
provide: LOCALE_ID as SafeInjectionToken<string>,
|
||||||
useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale,
|
useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale,
|
||||||
@@ -1542,6 +1545,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: MasterPasswordApiService,
|
useClass: MasterPasswordApiService,
|
||||||
deps: [ApiServiceAbstraction, LogService],
|
deps: [ApiServiceAbstraction, LogService],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: DocumentLangSetter,
|
||||||
|
useClass: DocumentLangSetter,
|
||||||
|
deps: [DOCUMENT, I18nServiceAbstraction],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: CipherEncryptionService,
|
provide: CipherEncryptionService,
|
||||||
useClass: DefaultCipherEncryptionService,
|
useClass: DefaultCipherEncryptionService,
|
||||||
|
|||||||
Reference in New Issue
Block a user