diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts index 3adaf9e276c..5e2be47b48a 100644 --- a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts +++ b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts @@ -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(); - const i18nService = mock(); + const i18nService = mock>(); const platformUtilsService = mock(); const themeStateService = mock(); 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"](); diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index fdfd5740ba0..9bc2c8f73b8 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -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(); diff --git a/apps/browser/src/platform/services/i18n.service.ts b/apps/browser/src/platform/services/i18n.service.ts index a27c3935d75..db0ecd5cbcd 100644 --- a/apps/browser/src/platform/services/i18n.service.ts +++ b/apps/browser/src/platform/services/i18n.service.ts @@ -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); } } diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index c4b79fcae58..430ae5dad29 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -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], }), diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json index e27d80e595a..cef43db29a1 100644 --- a/apps/browser/tsconfig.json +++ b/apps/browser/tsconfig.json @@ -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/*"], diff --git a/apps/cli/src/platform/services/i18n.service.ts b/apps/cli/src/platform/services/i18n.service.ts index 61e1ed6b09a..4c33fc74a7d 100644 --- a/apps/cli/src/platform/services/i18n.service.ts +++ b/apps/cli/src/platform/services/i18n.service.ts @@ -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 { constructor( systemLanguage: string, localesDirectory: string, diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index ae627e82e75..fbcbb20674a 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -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, diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json index 4cb450f9c69..e3409deafca 100644 --- a/apps/cli/tsconfig.json +++ b/apps/cli/tsconfig.json @@ -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"], diff --git a/apps/desktop/src/platform/services/i18n.main.service.ts b/apps/desktop/src/platform/services/i18n.main.service.ts index bb2d1b1c1c7..f574434630c 100644 --- a/apps/desktop/src/platform/services/i18n.main.service.ts +++ b/apps/desktop/src/platform/services/i18n.main.service.ts @@ -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 { constructor( systemLanguage: string, localesDirectory: string, diff --git a/apps/desktop/src/platform/services/i18n.renderer.service.ts b/apps/desktop/src/platform/services/i18n.renderer.service.ts index 18fe588f77d..fada5f30fa1 100644 --- a/apps/desktop/src/platform/services/i18n.renderer.service.ts +++ b/apps/desktop/src/platform/services/i18n.renderer.service.ts @@ -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, diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index 5fa0db4d80b..30699813327 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -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/*"], diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts index a5bd8ae3b07..f883cb289b0 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts @@ -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; let organizationApiService: MockProxy; let organizationUserApiService: MockProxy; - let i18nService: MockProxy; + let i18nService: MockProxy; let globalStateProvider: FakeGlobalStateProvider; let globalState: FakeGlobalState; diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index d1fa3f8ba8c..7d097916401 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -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: [] }), diff --git a/apps/web/src/app/core/i18n.service.ts b/apps/web/src/app/core/i18n.service.ts index 744b11d56e6..076d694b192 100644 --- a/apps/web/src/app/core/i18n.service.ts +++ b/apps/web/src/app/core/i18n.service.ts @@ -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, diff --git a/libs/common/src/platform/abstractions/i18n.service.ts b/libs/common/src/platform/abstractions/i18n.service.ts index a1b44d956a9..9172f03e087 100644 --- a/libs/common/src/platform/abstractions/i18n.service.ts +++ b/libs/common/src/platform/abstractions/i18n.service.ts @@ -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 extends TranslationService { abstract userSetLocale$: Observable; abstract locale$: Observable; abstract setLocale(locale: string): Promise; diff --git a/libs/common/src/platform/abstractions/translation.service.ts b/libs/common/src/platform/abstractions/translation.service.ts index 8a8faff1d8f..b148b3a8ade 100644 --- a/libs/common/src/platform/abstractions/translation.service.ts +++ b/libs/common/src/platform/abstractions/translation.service.ts @@ -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 = keyof { + [Index in keyof TClients]: Messages[TClients[Index]]; +}[number]; + +export abstract class TranslationService { abstract supportedTranslationLocales: string[]; abstract translationLocale: string; abstract collator: Intl.Collator; abstract localeNames: Map; - 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, + p1?: string | number, + p2?: string | number, + p3?: string | number, + ): string; + abstract translate(id: I18nKeys, p1?: string, p2?: string, p3?: string): string; } diff --git a/libs/common/src/platform/services/i18n.service.ts b/libs/common/src/platform/services/i18n.service.ts index aa2692d20b5..c399fad6c81 100644 --- a/libs/common/src/platform/services/i18n.service.ts +++ b/libs/common/src/platform/services/i18n.service.ts @@ -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(TRANSLATION_DISK, "locale", { deserializer: (value) => value, }); -export class I18nService extends TranslationService implements I18nServiceAbstraction { +export abstract class BaseI18nService + extends TranslationService + implements I18nServiceAbstraction +{ translationLocale: string; protected translationLocaleState: GlobalState; userSetLocale$: Observable; diff --git a/libs/common/src/platform/services/translation.service.ts b/libs/common/src/platform/services/translation.service.ts index 4ad8162af57..d3fd42da110 100644 --- a/libs/common/src/platform/services/translation.service.ts +++ b/libs/common/src/platform/services/translation.service.ts @@ -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 + implements TranslationServiceAbstraction +{ // 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, 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, + 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]) {