1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +00:00

[PM-6511] New i18n for angular (#8122)

* Use state provider to store preferred language

* migrate preferred language

* Use new i18n provider to get LOCAL_ID

* Fix preloaded english i18n

This is a mock service that forces english translations, it doesn't need the i18n interface that allows changing of locales.

* PR improvements

* Fixup merge
This commit is contained in:
Matt Gibson
2024-03-11 12:59:19 -05:00
committed by GitHub
parent c10a59b019
commit f4150ffda6
25 changed files with 278 additions and 106 deletions

View File

@@ -195,12 +195,12 @@ import { BrowserStateService as StateServiceAbstraction } from "../platform/serv
import { BrowserConfigService } from "../platform/services/browser-config.service";
import { BrowserCryptoService } from "../platform/services/browser-crypto.service";
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
import { BrowserI18nService } from "../platform/services/browser-i18n.service";
import BrowserLocalStorageService from "../platform/services/browser-local-storage.service";
import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service";
import BrowserMessagingService from "../platform/services/browser-messaging.service";
import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service";
import { BrowserStateService } from "../platform/services/browser-state.service";
import I18nService from "../platform/services/i18n.service";
import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service";
import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider";
import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service";
@@ -462,7 +462,7 @@ export default class MainBackground {
},
self,
);
this.i18nService = new BrowserI18nService(BrowserApi.getUILanguage(), this.stateService);
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
this.cryptoService = new BrowserCryptoService(
this.keyGenerationService,
this.cryptoFunctionService,
@@ -969,7 +969,7 @@ export default class MainBackground {
await this.stateService.init();
await this.vaultTimeoutService.init(true);
await (this.i18nService as BrowserI18nService).init();
await (this.i18nService as I18nService).init();
await (this.eventUploadService as EventUploadService).init(true);
await this.runtimeBackground.init();
await this.notificationBackground.init();

View File

@@ -4,6 +4,10 @@ import { I18nService as BaseI18nService } from "@bitwarden/common/platform/servi
import I18nService from "../../services/i18n.service";
import { FactoryOptions, CachedServices, factory } from "./factory-options";
import {
GlobalStateProviderInitOptions,
globalStateProviderFactory,
} from "./global-state-provider.factory";
type I18nServiceFactoryOptions = FactoryOptions & {
i18nServiceOptions: {
@@ -11,7 +15,7 @@ type I18nServiceFactoryOptions = FactoryOptions & {
};
};
export type I18nServiceInitOptions = I18nServiceFactoryOptions;
export type I18nServiceInitOptions = I18nServiceFactoryOptions & GlobalStateProviderInitOptions;
export async function i18nServiceFactory(
cache: { i18nService?: AbstractI18nService } & CachedServices,
@@ -21,7 +25,11 @@ export async function i18nServiceFactory(
cache,
"i18nService",
opts,
() => new I18nService(opts.i18nServiceOptions.systemLanguage),
async () =>
new I18nService(
opts.i18nServiceOptions.systemLanguage,
await globalStateProviderFactory(cache, opts),
),
);
if (!(service as BaseI18nService as any).inited) {
await (service as BaseI18nService).init();

View File

@@ -1,20 +0,0 @@
import { ReplaySubject } from "rxjs";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { browserSession, sessionSync } from "../decorators/session-sync-observable";
import I18nService from "./i18n.service";
@browserSession
export class BrowserI18nService extends I18nService {
@sessionSync({ initializer: (s: string) => s })
protected _locale: ReplaySubject<string>;
constructor(
systemLanguage: string,
private stateService: StateService,
) {
super(systemLanguage);
}
}

View File

@@ -1,12 +1,18 @@
import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
export default class I18nService extends BaseI18nService {
constructor(systemLanguage: string) {
super(systemLanguage, null, async (formattedLocale: string) => {
// Deprecated
const file = await fetch(this.localesDirectory + formattedLocale + "/messages.json");
return await file.json();
});
constructor(systemLanguage: string, globalStateProvider: GlobalStateProvider) {
super(
systemLanguage,
null,
async (formattedLocale: string) => {
// Deprecated
const file = await fetch(this.localesDirectory + formattedLocale + "/messages.json");
return await file.json();
},
globalStateProvider,
);
// Please leave 'en' where it is, as it's our fallback language in case no translation can be found
this.supportedTranslationLocales = [

View File

@@ -1,4 +1,4 @@
import { APP_INITIALIZER, LOCALE_ID, NgModule, NgZone } from "@angular/core";
import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core";
import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards";
import { ThemingService } from "@bitwarden/angular/platform/services/theming/theming.service";
@@ -60,10 +60,7 @@ import {
} from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
StateService as BaseStateServiceAbstraction,
StateService,
} from "@bitwarden/common/platform/abstractions/state.service";
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
import {
AbstractMemoryStorageService,
AbstractStorageService,
@@ -75,7 +72,11 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
import { DerivedStateProvider, StateProvider } from "@bitwarden/common/platform/state";
import {
DerivedStateProvider,
GlobalStateProvider,
StateProvider,
} from "@bitwarden/common/platform/state";
import { SearchService } from "@bitwarden/common/services/search.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
@@ -112,11 +113,11 @@ import { BrowserStateService as StateServiceAbstraction } from "../../platform/s
import { BrowserConfigService } from "../../platform/services/browser-config.service";
import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service";
import { BrowserFileDownloadService } from "../../platform/services/browser-file-download.service";
import { BrowserI18nService } from "../../platform/services/browser-i18n.service";
import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service";
import BrowserMessagingPrivateModePopupService from "../../platform/services/browser-messaging-private-mode-popup.service";
import BrowserMessagingService from "../../platform/services/browser-messaging.service";
import { BrowserStateService } from "../../platform/services/browser-state.service";
import I18nService from "../../platform/services/i18n.service";
import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider";
import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service";
import { BrowserSendService } from "../../services/browser-send.service";
@@ -158,11 +159,6 @@ function getBgService<T>(service: keyof MainBackground) {
DebounceNavigationService,
DialogService,
PopupCloseWarningService,
{
provide: LOCALE_ID,
useFactory: () => getBgService<I18nServiceAbstraction>("i18nService")().translationLocale,
deps: [],
},
{
provide: APP_INITIALIZER,
useFactory: (initService: InitService) => initService.init(),
@@ -254,10 +250,10 @@ function getBgService<T>(service: keyof MainBackground) {
{ provide: TokenService, useFactory: getBgService<TokenService>("tokenService"), deps: [] },
{
provide: I18nServiceAbstraction,
useFactory: (stateService: BrowserStateService) => {
return new BrowserI18nService(BrowserApi.getUILanguage(), stateService);
useFactory: (globalStateProvider: GlobalStateProvider) => {
return new I18nService(BrowserApi.getUILanguage(), globalStateProvider);
},
deps: [StateService],
deps: [GlobalStateProvider],
},
{
provide: CryptoService,

View File

@@ -231,7 +231,6 @@ export class Main {
p = path.join(process.env.HOME, ".config/Bitwarden CLI");
}
this.i18nService = new I18nService("en", "./locales");
this.platformUtilsService = new CliPlatformUtilsService(ClientType.Cli, packageJson);
this.logService = new ConsoleLogService(
this.platformUtilsService.isDev(),
@@ -270,6 +269,8 @@ export class Main {
storageServiceProvider,
);
this.i18nService = new I18nService("en", "./locales", this.globalStateProvider);
this.singleUserStateProvider = new DefaultSingleUserStateProvider(
storageServiceProvider,
stateEventRegistrarService,
@@ -665,8 +666,7 @@ export class Main {
await this.stateService.init();
this.containerService.attachToGlobal(global);
await this.environmentService.setUrlsFromStorage();
const locale = await this.stateService.getLocale();
await this.i18nService.init(locale);
await this.i18nService.init();
this.twoFactorService.init();
this.configService.init();

View File

@@ -2,18 +2,28 @@ import * as fs from "fs";
import * as path from "path";
import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
export class I18nService extends BaseI18nService {
constructor(systemLanguage: string, localesDirectory: string) {
super(systemLanguage, localesDirectory, (formattedLocale: string) => {
const filePath = path.join(
__dirname,
this.localesDirectory + "/" + formattedLocale + "/messages.json",
);
const localesJson = fs.readFileSync(filePath, "utf8");
const locales = JSON.parse(localesJson.replace(/^\uFEFF/, "")); // strip the BOM
return Promise.resolve(locales);
});
constructor(
systemLanguage: string,
localesDirectory: string,
globalStateProvider: GlobalStateProvider,
) {
super(
systemLanguage,
localesDirectory,
(formattedLocale: string) => {
const filePath = path.join(
__dirname,
this.localesDirectory + "/" + formattedLocale + "/messages.json",
);
const localesJson = fs.readFileSync(filePath, "utf8");
const locales = JSON.parse(localesJson.replace(/^\uFEFF/, "")); // strip the BOM
return Promise.resolve(locales);
},
globalStateProvider,
);
this.supportedTranslationLocales = ["en"];
}

View File

@@ -264,7 +264,7 @@ export class SettingsComponent implements OnInit {
enableDuckDuckGoBrowserIntegration:
await this.stateService.getEnableDuckDuckGoBrowserIntegration(),
theme: await this.stateService.getTheme(),
locale: (await this.stateService.getLocale()) ?? null,
locale: await firstValueFrom(this.i18nService.locale$),
};
this.form.setValue(initialValues, { emitEvent: false });
@@ -553,7 +553,7 @@ export class SettingsComponent implements OnInit {
}
async saveLocale() {
await this.stateService.setLocale(this.form.value.locale);
await this.i18nService.setLocale(this.form.value.locale);
}
async saveTheme() {

View File

@@ -52,8 +52,7 @@ export class InitService {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.syncService.fullSync(true);
await this.vaultTimeoutService.init(true);
const locale = await this.stateService.getLocale();
await (this.i18nService as I18nRendererService).init(locale);
await (this.i18nService as I18nRendererService).init();
(this.eventUploadService as EventUploadService).init(true);
this.twoFactorService.init();
setTimeout(() => this.notificationsService.init(), 3000);

View File

@@ -43,7 +43,7 @@ import { GlobalState } from "@bitwarden/common/platform/models/domain/global-sta
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { SystemService } from "@bitwarden/common/platform/services/system.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
@@ -104,7 +104,7 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
{
provide: I18nServiceAbstraction,
useClass: I18nRendererService,
deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY],
deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider],
},
{
provide: MessagingServiceAbstraction,

View File

@@ -97,7 +97,6 @@ export class Main {
}
this.logService = new ElectronLogMainService(null, app.getPath("userData"));
this.i18nService = new I18nMainService("en", "./locales/");
const storageDefaults: any = {};
// Default vault timeout to "on restart", and action to "lock"
@@ -112,6 +111,8 @@ export class Main {
);
const globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider);
this.i18nService = new I18nMainService("en", "./locales/", globalStateProvider);
const accountService = new AccountServiceImplementation(
new NoopMessagingService(),
this.logService,
@@ -218,8 +219,7 @@ export class Main {
this.migrationRunner.run().then(
async () => {
await this.windowMain.init();
const locale = await this.stateService.getLocale();
await this.i18nService.init(locale != null ? locale : app.getLocale());
await this.i18nService.init();
this.messagingMain.init();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises

View File

@@ -1,14 +1,22 @@
import * as fs from "fs";
import * as path from "path";
import { ipcMain } from "electron";
import { app, ipcMain } from "electron";
import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
export class I18nMainService extends BaseI18nService {
constructor(systemLanguage: string, localesDirectory: string) {
super(systemLanguage, localesDirectory, (formattedLocale: string) =>
this.readLanguageFile(formattedLocale),
constructor(
systemLanguage: string,
localesDirectory: string,
globalStateProvider: GlobalStateProvider,
) {
super(
systemLanguage,
localesDirectory,
(formattedLocale: string) => this.readLanguageFile(formattedLocale),
globalStateProvider,
);
ipcMain.handle("getLanguageFile", async (event, formattedLocale: string) =>
@@ -76,6 +84,12 @@ export class I18nMainService extends BaseI18nService {
];
}
override async init(): Promise<void> {
// Set system language to electron language
this.systemLanguage = app.getLocale();
await super.init();
}
private readLanguageFile(formattedLocale: string): Promise<any> {
// Check that the provided locale only contains letters and dashes and underscores to avoid possible path traversal
if (!/^[a-zA-Z_-]+$/.test(formattedLocale)) {

View File

@@ -1,10 +1,20 @@
import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
export class I18nRendererService extends BaseI18nService {
constructor(systemLanguage: string, localesDirectory: string) {
super(systemLanguage, localesDirectory, (formattedLocale: string) => {
return ipc.platform.getLanguageFile(formattedLocale);
});
constructor(
systemLanguage: string,
localesDirectory: string,
globalStateProvider: GlobalStateProvider,
) {
super(
systemLanguage,
localesDirectory,
(formattedLocale: string) => {
return ipc.platform.getLanguageFile(formattedLocale);
},
globalStateProvider,
);
// Please leave 'en' where it is, as it's our fallback language in case no translation can be found
this.supportedTranslationLocales = [

View File

@@ -29,6 +29,7 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
/* eslint-disable import/no-restricted-paths -- Implementation for memory storage */
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
/* eslint-enable import/no-restricted-paths -- Implementation for memory storage */
@@ -74,7 +75,7 @@ import { WebPlatformUtilsService } from "./web-platform-utils.service";
{
provide: I18nServiceAbstraction,
useClass: I18nService,
deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY],
deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider],
},
{ provide: AbstractStorageService, useClass: HtmlStorageService },
{

View File

@@ -1,20 +1,30 @@
import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
import { SupportedTranslationLocales } from "../../translation-constants";
export class I18nService extends BaseI18nService {
constructor(systemLanguage: string, localesDirectory: string) {
super(systemLanguage || "en-US", localesDirectory, async (formattedLocale: string) => {
const filePath =
this.localesDirectory +
"/" +
formattedLocale +
"/messages.json?cache=" +
process.env.CACHE_TAG;
const localesResult = await fetch(filePath);
const locales = await localesResult.json();
return locales;
});
constructor(
systemLanguage: string,
localesDirectory: string,
globalStateProvider: GlobalStateProvider,
) {
super(
systemLanguage || "en-US",
localesDirectory,
async (formattedLocale: string) => {
const filePath =
this.localesDirectory +
"/" +
formattedLocale +
"/messages.json?cache=" +
process.env.CACHE_TAG;
const localesResult = await fetch(filePath);
const locales = await localesResult.json();
return locales;
},
globalStateProvider,
);
this.supportedTranslationLocales = SupportedTranslationLocales;
}

View File

@@ -50,8 +50,7 @@ export class InitService {
setTimeout(() => this.notificationsService.init(), 3000);
await this.vaultTimeoutService.init(true);
const locale = await this.stateService.getLocale();
await (this.i18nService as I18nService).init(locale);
await (this.i18nService as I18nService).init();
(this.eventUploadService as EventUploadService).init(true);
this.twoFactorService.init();
const htmlEl = this.win.document.documentElement;

View File

@@ -1,16 +1,23 @@
import { APP_INITIALIZER, NgModule } from "@angular/core";
import { Observable, of } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
import { TranslationService } from "@bitwarden/common/platform/services/translation.service";
import eng from "../../../locales/en/messages.json";
class PreloadedEnglishI18nService extends BaseI18nService {
class PreloadedEnglishI18nService extends TranslationService implements I18nService {
translationLocale = "en";
locale$: Observable<string> = of("en");
constructor() {
super("en", "", () => {
return Promise.resolve(eng);
});
}
setLocale(): Promise<void> {
throw new Error("Method not implemented.");
}
}
function i18nInitializer(i18nService: I18nService): () => Promise<void> {

View File

@@ -142,7 +142,7 @@ export class PreferencesComponent implements OnInit {
),
enableFavicons: !(await this.settingsService.getDisableFavicon()),
theme: await this.stateService.getTheme(),
locale: (await this.stateService.getLocale()) ?? null,
locale: (await firstValueFrom(this.i18nService.locale$)) ?? null,
};
this.startingLocale = initialFormValues.locale;
this.startingTheme = initialFormValues.theme;
@@ -169,7 +169,7 @@ export class PreferencesComponent implements OnInit {
await this.themingService.updateConfiguredTheme(values.theme);
this.startingTheme = values.theme;
}
await this.stateService.setLocale(values.locale);
await this.i18nService.setLocale(values.locale);
if (values.locale !== this.startingLocale) {
window.location.reload();
} else {