mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 14:34:02 +00:00
wip typesafe i18n
This commit is contained in:
@@ -19,7 +19,7 @@ import {
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CloudEnvironment } from "@bitwarden/common/platform/services/default-environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/services/i18n.service";
|
||||
import { BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import {
|
||||
FakeStateProvider,
|
||||
@@ -73,7 +73,7 @@ describe("OverlayBackground", () => {
|
||||
}),
|
||||
);
|
||||
const autofillSettingsService = mock<AutofillSettingsService>();
|
||||
const i18nService = mock<I18nService>();
|
||||
const i18nService = mock<BaseI18nService<["browser"]>>();
|
||||
const platformUtilsService = mock<BrowserPlatformUtilsService>();
|
||||
const themeStateService = mock<ThemeStateService>();
|
||||
const initOverlayElementPorts = async (options = { initList: true, initButton: true }) => {
|
||||
@@ -429,7 +429,9 @@ describe("OverlayBackground", () => {
|
||||
it("will query the overlay page translations if they have not been queried", () => {
|
||||
overlayBackground["overlayPageTranslations"] = undefined;
|
||||
jest.spyOn(overlayBackground as any, "getTranslations");
|
||||
jest.spyOn(overlayBackground["i18nService"], "translate").mockImplementation((key) => key);
|
||||
jest
|
||||
.spyOn(overlayBackground["i18nService"], "translate")
|
||||
.mockImplementation((key: any) => key);
|
||||
jest.spyOn(BrowserApi, "getUILanguage").mockReturnValue("en");
|
||||
|
||||
const translations = overlayBackground["getTranslations"]();
|
||||
|
||||
@@ -250,7 +250,7 @@ import { BrowserEnvironmentService } from "../platform/services/browser-environm
|
||||
import BrowserLocalStorageService from "../platform/services/browser-local-storage.service";
|
||||
import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service";
|
||||
import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service";
|
||||
import I18nService from "../platform/services/i18n.service";
|
||||
import BrowserI18nService from "../platform/services/i18n.service";
|
||||
import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service";
|
||||
import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service";
|
||||
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
|
||||
@@ -624,7 +624,7 @@ export default class MainBackground {
|
||||
this.logService,
|
||||
);
|
||||
|
||||
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
|
||||
this.i18nService = new BrowserI18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
|
||||
|
||||
this.biometricsService = new BackgroundBrowserBiometricsService(
|
||||
runtimeNativeMessagingBackground,
|
||||
@@ -1275,7 +1275,7 @@ export default class MainBackground {
|
||||
}
|
||||
await Promise.all(setUserKeyInMemoryPromises);
|
||||
|
||||
await (this.i18nService as I18nService).init();
|
||||
await (this.i18nService as BrowserI18nService).init();
|
||||
(this.eventUploadService as EventUploadService).init(true);
|
||||
|
||||
this.popupViewCacheBackgroundService.startObservingTabChanges();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
|
||||
import { I18nKeys } from "@bitwarden/common/platform/abstractions/translation.service";
|
||||
import { BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
|
||||
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
|
||||
|
||||
export default class I18nService extends BaseI18nService {
|
||||
export default class BrowserI18nService extends BaseI18nService<["browser"]> {
|
||||
constructor(systemLanguage: string, globalStateProvider: GlobalStateProvider) {
|
||||
super(
|
||||
systemLanguage,
|
||||
@@ -80,11 +81,11 @@ export default class I18nService extends BaseI18nService {
|
||||
];
|
||||
}
|
||||
|
||||
t(id: string, p1?: string, p2?: string, p3?: string): string {
|
||||
t(id: I18nKeys<["browser"]>, p1?: string, p2?: string, p3?: string): string {
|
||||
return this.translate(id, p1, p2, p3);
|
||||
}
|
||||
|
||||
translate(id: string, p1?: string, p2?: string, p3?: string): string {
|
||||
translate(id: I18nKeys<["browser"]>, p1?: string, p2?: string, p3?: string): string {
|
||||
if (this.localesDirectory == null) {
|
||||
const placeholders: string[] = [];
|
||||
if (p1 != null) {
|
||||
@@ -104,6 +105,6 @@ export default class I18nService extends BaseI18nService {
|
||||
}
|
||||
}
|
||||
|
||||
return super.translate(id, p1, p2, p3);
|
||||
return super.translate(id as any, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ import { BrowserEnvironmentService } from "../../platform/services/browser-envir
|
||||
import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service";
|
||||
import BrowserMemoryStorageService from "../../platform/services/browser-memory-storage.service";
|
||||
import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service";
|
||||
import I18nService from "../../platform/services/i18n.service";
|
||||
import BrowserI18nService from "../../platform/services/i18n.service";
|
||||
import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service";
|
||||
import { BrowserSdkClientFactory } from "../../platform/services/sdk/browser-sdk-client-factory";
|
||||
import { ForegroundTaskSchedulerService } from "../../platform/services/task-scheduler/foreground-task-scheduler.service";
|
||||
@@ -205,7 +205,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: I18nServiceAbstraction,
|
||||
useFactory: (globalStateProvider: GlobalStateProvider) => {
|
||||
return new I18nService(BrowserApi.getUILanguage(), globalStateProvider);
|
||||
return new BrowserI18nService(BrowserApi.getUILanguage(), globalStateProvider);
|
||||
},
|
||||
deps: [GlobalStateProvider],
|
||||
}),
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"lib": ["ES2021.String"],
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"@bitwarden/admin-console/common": ["../../libs/admin-console/src/common"],
|
||||
"@bitwarden/angular/*": ["../../libs/angular/src/*"],
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
|
||||
import { BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
|
||||
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
|
||||
|
||||
export class I18nService extends BaseI18nService {
|
||||
import CliMessages from "../../locales/en/messages.json";
|
||||
|
||||
type CliMessages = typeof CliMessages;
|
||||
|
||||
export class CliI18nService extends BaseI18nService<CliMessages> {
|
||||
constructor(
|
||||
systemLanguage: string,
|
||||
localesDirectory: string,
|
||||
|
||||
@@ -166,7 +166,7 @@ import {
|
||||
import { flagEnabled } from "../platform/flags";
|
||||
import { CliPlatformUtilsService } from "../platform/services/cli-platform-utils.service";
|
||||
import { ConsoleLogService } from "../platform/services/console-log.service";
|
||||
import { I18nService } from "../platform/services/i18n.service";
|
||||
import { CliI18nService } from "../platform/services/i18n.service";
|
||||
import { LowdbStorageService } from "../platform/services/lowdb-storage.service";
|
||||
import { NodeApiService } from "../platform/services/node-api.service";
|
||||
import { NodeEnvSecureStorageService } from "../platform/services/node-env-secure-storage.service";
|
||||
@@ -189,7 +189,7 @@ export class ServiceContainer {
|
||||
secureStorageService: NodeEnvSecureStorageService;
|
||||
memoryStorageService: MemoryStorageService;
|
||||
memoryStorageForStateProviders: MemoryStorageServiceForStateProviders;
|
||||
i18nService: I18nService;
|
||||
i18nService: CliI18nService;
|
||||
platformUtilsService: CliPlatformUtilsService;
|
||||
keyService: KeyService;
|
||||
tokenService: TokenService;
|
||||
@@ -331,7 +331,7 @@ export class ServiceContainer {
|
||||
storageServiceProvider,
|
||||
);
|
||||
|
||||
this.i18nService = new I18nService("en", "./locales", this.globalStateProvider);
|
||||
this.i18nService = new CliI18nService("en", "./locales", this.globalStateProvider);
|
||||
|
||||
this.singleUserStateProvider = new DefaultSingleUserStateProvider(
|
||||
storageServiceProvider,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"allowJs": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"@bitwarden/common/spec": ["../../libs/common/spec"],
|
||||
"@bitwarden/admin-console/common": ["../../libs/admin-console/src/common"],
|
||||
|
||||
@@ -3,10 +3,14 @@ import * as path from "path";
|
||||
|
||||
import { app, ipcMain } from "electron";
|
||||
|
||||
import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
|
||||
import { BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
|
||||
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
|
||||
|
||||
export class I18nMainService extends BaseI18nService {
|
||||
import type DesktopMessages from "../../locales/en/messages.json";
|
||||
|
||||
type DesktopMessages = typeof DesktopMessages;
|
||||
|
||||
export class I18nMainService extends BaseI18nService<DesktopMessages> {
|
||||
constructor(
|
||||
systemLanguage: string,
|
||||
localesDirectory: string,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
|
||||
import { BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
|
||||
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
|
||||
|
||||
export class I18nRendererService extends BaseI18nService {
|
||||
export class I18nRendererService extends BaseI18nService<["desktop"]> {
|
||||
constructor(
|
||||
systemLanguage: string,
|
||||
localesDirectory: string,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"sourceMap": true,
|
||||
"types": [],
|
||||
"baseUrl": ".",
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"@bitwarden/admin-console/common": ["../../libs/admin-console/src/common"],
|
||||
"@bitwarden/angular/*": ["../../libs/angular/src/*"],
|
||||
|
||||
@@ -17,7 +17,7 @@ import { FakeGlobalState } from "@bitwarden/common/spec/fake-state";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { I18nService } from "../../core/i18n.service";
|
||||
import { WebI18nService } from "../../core/i18n.service";
|
||||
|
||||
import {
|
||||
AcceptOrganizationInviteService,
|
||||
@@ -36,7 +36,7 @@ describe("AcceptOrganizationInviteService", () => {
|
||||
let logService: MockProxy<LogService>;
|
||||
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
|
||||
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let i18nService: MockProxy<WebI18nService>;
|
||||
let globalStateProvider: FakeGlobalStateProvider;
|
||||
let globalState: FakeGlobalState<OrganizationInvite>;
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ import {
|
||||
} from "../auth";
|
||||
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
|
||||
import { HtmlStorageService } from "../core/html-storage.service";
|
||||
import { I18nService } from "../core/i18n.service";
|
||||
import { WebI18nService } from "../core/i18n.service";
|
||||
import { WebBiometricsService } from "../key-management/web-biometric.service";
|
||||
import { WebEnvironmentService } from "../platform/web-environment.service";
|
||||
import { WebMigrationRunner } from "../platform/web-migration-runner";
|
||||
@@ -133,7 +133,7 @@ const safeProviders: SafeProvider[] = [
|
||||
}),
|
||||
safeProvider({
|
||||
provide: I18nServiceAbstraction,
|
||||
useClass: I18nService,
|
||||
useClass: WebI18nService,
|
||||
deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider],
|
||||
}),
|
||||
safeProvider({ provide: AbstractStorageService, useClass: HtmlStorageService, deps: [] }),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service";
|
||||
import { 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 {
|
||||
export class WebI18nService extends BaseI18nService<["web"]> {
|
||||
constructor(
|
||||
systemLanguage: string,
|
||||
localesDirectory: string,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { TranslationService } from "./translation.service";
|
||||
import { ClientTuple, TranslationService } from "./translation.service";
|
||||
|
||||
export abstract class I18nService extends TranslationService {
|
||||
export abstract class I18nService<T extends ClientTuple> extends TranslationService<T> {
|
||||
abstract userSetLocale$: Observable<string | undefined>;
|
||||
abstract locale$: Observable<string>;
|
||||
abstract setLocale(locale: string): Promise<void>;
|
||||
|
||||
@@ -1,8 +1,40 @@
|
||||
export abstract class TranslationService {
|
||||
// eslint-disable-next-line import/no-restricted-paths
|
||||
import type BrowserMessages from "../../../../../apps/browser/src/_locales/en/messages.json";
|
||||
// eslint-disable-next-line import/no-restricted-paths
|
||||
import type CliMessages from "../../../../../apps/cli/src/locales/en/messages.json";
|
||||
// eslint-disable-next-line import/no-restricted-paths
|
||||
import type DesktopMessages from "../../../../../apps/desktop/src/locales/en/messages.json";
|
||||
// eslint-disable-next-line import/no-restricted-paths
|
||||
import type WebMessages from "../../../../../apps/web/src/locales/en/messages.json";
|
||||
|
||||
type BrowserMessages = typeof BrowserMessages;
|
||||
type CliMessages = typeof CliMessages;
|
||||
type DesktopMessages = typeof DesktopMessages;
|
||||
type WebMessages = typeof WebMessages;
|
||||
|
||||
type Messages = {
|
||||
browser: BrowserMessages;
|
||||
cli: CliMessages;
|
||||
desktop: DesktopMessages;
|
||||
web: WebMessages;
|
||||
};
|
||||
|
||||
export type ClientTuple = (keyof Messages)[];
|
||||
|
||||
export type I18nKeys<TClients extends ClientTuple> = keyof {
|
||||
[Index in keyof TClients]: Messages[TClients[Index]];
|
||||
}[number];
|
||||
|
||||
export abstract class TranslationService<TClients extends ClientTuple> {
|
||||
abstract supportedTranslationLocales: string[];
|
||||
abstract translationLocale: string;
|
||||
abstract collator: Intl.Collator;
|
||||
abstract localeNames: Map<string, string>;
|
||||
abstract t(id: string, p1?: string | number, p2?: string | number, p3?: string | number): string;
|
||||
abstract translate(id: string, p1?: string, p2?: string, p3?: string): string;
|
||||
abstract t(
|
||||
id: I18nKeys<TClients>,
|
||||
p1?: string | number,
|
||||
p2?: string | number,
|
||||
p3?: string | number,
|
||||
): string;
|
||||
abstract translate(id: I18nKeys<TClients>, p1?: string, p2?: string, p3?: string): string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Observable, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { I18nService as I18nServiceAbstraction } from "../abstractions/i18n.service";
|
||||
import { ClientTuple } from "../abstractions/translation.service";
|
||||
import { GlobalState, GlobalStateProvider, KeyDefinition, TRANSLATION_DISK } from "../state";
|
||||
|
||||
import { TranslationService } from "./translation.service";
|
||||
@@ -9,7 +10,10 @@ const LOCALE_KEY = new KeyDefinition<string>(TRANSLATION_DISK, "locale", {
|
||||
deserializer: (value) => value,
|
||||
});
|
||||
|
||||
export class I18nService extends TranslationService implements I18nServiceAbstraction {
|
||||
export abstract class BaseI18nService<T extends ClientTuple>
|
||||
extends TranslationService<T>
|
||||
implements I18nServiceAbstraction<T>
|
||||
{
|
||||
translationLocale: string;
|
||||
protected translationLocaleState: GlobalState<string>;
|
||||
userSetLocale$: Observable<string | undefined>;
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { TranslationService as TranslationServiceAbstraction } from "../abstractions/translation.service";
|
||||
import {
|
||||
ClientTuple,
|
||||
I18nKeys,
|
||||
TranslationService as TranslationServiceAbstraction,
|
||||
} from "../abstractions/translation.service";
|
||||
|
||||
export abstract class TranslationService implements TranslationServiceAbstraction {
|
||||
export abstract class TranslationService<T extends ClientTuple>
|
||||
implements TranslationServiceAbstraction<T>
|
||||
{
|
||||
// First locale is the default (English)
|
||||
supportedTranslationLocales: string[] = ["en"];
|
||||
defaultLocale = "en";
|
||||
@@ -121,11 +127,16 @@ export abstract class TranslationService implements TranslationServiceAbstractio
|
||||
}
|
||||
}
|
||||
|
||||
t(id: string, p1?: string, p2?: string, p3?: string): string {
|
||||
t(id: I18nKeys<ClientTuple>, p1?: string, p2?: string, p3?: string): string {
|
||||
return this.translate(id, p1, p2, p3);
|
||||
}
|
||||
|
||||
translate(id: string, p1?: string | number, p2?: string | number, p3?: string | number): string {
|
||||
translate(
|
||||
id: I18nKeys<ClientTuple>,
|
||||
p1?: string | number,
|
||||
p2?: string | number,
|
||||
p3?: string | number,
|
||||
): string {
|
||||
let result: string;
|
||||
// eslint-disable-next-line
|
||||
if (this.localeMessages.hasOwnProperty(id) && this.localeMessages[id]) {
|
||||
|
||||
Reference in New Issue
Block a user