diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index 0d60a1140f..f45a60cadc 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -17,7 +17,7 @@ import { EventUploadService } from "@bitwarden/common/services/event/event-uploa import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { I18nService } from "../../platform/services/i18n.service"; +import { I18nRendererService } from "../../platform/services/i18n.renderer.service"; import { NativeMessagingService } from "../../services/native-messaging.service"; @Injectable() @@ -51,7 +51,7 @@ export class InitService { this.syncService.fullSync(true); await this.vaultTimeoutService.init(true); const locale = await this.stateService.getLocale(); - await (this.i18nService as I18nService).init(locale); + await (this.i18nService as I18nRendererService).init(locale); (this.eventUploadService as EventUploadService).init(true); this.twoFactorService.init(); setTimeout(() => this.notificationsService.init(), 3000); diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index c586d8677c..24e3d91ab0 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -48,7 +48,7 @@ import { ElectronRendererSecureStorageService } from "../../platform/services/el import { ElectronRendererStorageService } from "../../platform/services/electron-renderer-storage.service"; import { ElectronStateService } from "../../platform/services/electron-state.service"; import { ElectronStateService as ElectronStateServiceAbstraction } from "../../platform/services/electron-state.service.abstraction"; -import { I18nService } from "../../platform/services/i18n.service"; +import { I18nRendererService } from "../../platform/services/i18n.renderer.service"; import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service"; import { NativeMessageHandlerService } from "../../services/native-message-handler.service"; import { NativeMessagingService } from "../../services/native-messaging.service"; @@ -91,7 +91,7 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK"); }, { provide: I18nServiceAbstraction, - useClass: I18nService, + useClass: I18nRendererService, deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY], }, { diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 136c178cd8..f74d6e5fa9 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -21,12 +21,12 @@ import { DesktopCredentialStorageListener } from "./platform/main/desktop-creden import { ElectronLogService } from "./platform/services/electron-log.service"; import { ElectronStateService } from "./platform/services/electron-state.service"; import { ElectronStorageService } from "./platform/services/electron-storage.service"; -import { I18nService } from "./platform/services/i18n.service"; +import { I18nMainService } from "./platform/services/i18n.main.service"; import { ElectronMainMessagingService } from "./services/electron-main-messaging.service"; export class Main { logService: ElectronLogService; - i18nService: I18nService; + i18nService: I18nMainService; storageService: ElectronStorageService; memoryStorageService: MemoryStorageService; messagingService: ElectronMainMessagingService; @@ -76,7 +76,7 @@ export class Main { } this.logService = new ElectronLogService(null, app.getPath("userData")); - this.i18nService = new I18nService("en", "./locales/"); + this.i18nService = new I18nMainService("en", "./locales/"); const storageDefaults: any = {}; // Default vault timeout to "on restart", and action to "lock" diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts index 4d5e1c8e0c..5430d3fb35 100644 --- a/apps/desktop/src/platform/preload.ts +++ b/apps/desktop/src/platform/preload.ts @@ -39,6 +39,9 @@ export default { ipcRenderer.on("systemThemeUpdated", (_event, theme: ThemeType) => callback(theme)); }, + getLanguageFile: (formattedLocale: string): Promise => + ipcRenderer.invoke("getLanguageFile", formattedLocale), + sendMessage: (message: { command: string } & any) => ipcRenderer.send("messagingService", message), onMessage: (callback: (message: { command: string } & any) => void) => { diff --git a/apps/desktop/src/platform/services/i18n.main.service.ts b/apps/desktop/src/platform/services/i18n.main.service.ts new file mode 100644 index 0000000000..2f598f83b6 --- /dev/null +++ b/apps/desktop/src/platform/services/i18n.main.service.ts @@ -0,0 +1,90 @@ +import { promises as fs } from "fs"; +import * as path from "path"; + +import { ipcMain } from "electron"; + +import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service"; + +export class I18nMainService extends BaseI18nService { + constructor(systemLanguage: string, localesDirectory: string) { + super(systemLanguage, localesDirectory, (formattedLocale: string) => + this.readLanguageFile(formattedLocale) + ); + + ipcMain.handle("getLanguageFile", async (event, formattedLocale: string) => + this.readLanguageFile(formattedLocale) + ); + + // Please leave 'en' where it is, as it's our fallback language in case no translation can be found + this.supportedTranslationLocales = [ + "en", + "af", + "ar", + "az", + "be", + "bg", + "bn", + "bs", + "ca", + "cs", + "da", + "de", + "el", + "en-GB", + "en-IN", + "eo", + "es", + "et", + "eu", + "fa", + "fi", + "fil", + "fr", + "he", + "hi", + "hr", + "hu", + "id", + "it", + "ja", + "ka", + "km", + "kn", + "ko", + "lv", + "me", + "ml", + "nb", + "nl", + "nn", + "pl", + "pt-BR", + "pt-PT", + "ro", + "ru", + "si", + "sk", + "sl", + "sr", + "sv", + "th", + "tr", + "uk", + "vi", + "zh-CN", + "zh-TW", + ]; + } + + private async readLanguageFile(formattedLocale: string): Promise { + // Check that the provided locale only contains letters and dashes and underscores to avoid possible path traversal + if (!/^[a-zA-Z_-]+$/.test(formattedLocale)) { + return Promise.resolve({}); + } + + const filePath = path.join(__dirname, this.localesDirectory, formattedLocale, "messages.json"); + const localesJson = await fs.readFile(filePath, "utf8"); + const locales = JSON.parse(localesJson.replace(/^\uFEFF/, "")); // strip the BOM + return Promise.resolve(locales); + } +} diff --git a/apps/desktop/src/platform/services/i18n.service.ts b/apps/desktop/src/platform/services/i18n.renderer.service.ts similarity index 71% rename from apps/desktop/src/platform/services/i18n.service.ts rename to apps/desktop/src/platform/services/i18n.renderer.service.ts index ddcc9ca972..906f53566e 100644 --- a/apps/desktop/src/platform/services/i18n.service.ts +++ b/apps/desktop/src/platform/services/i18n.renderer.service.ts @@ -1,18 +1,9 @@ -import * as fs from "fs"; -import * as path from "path"; - import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service"; -export class I18nService extends BaseI18nService { +export class I18nRendererService 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); + return ipc.platform.getLanguageFile(formattedLocale); }); // Please leave 'en' where it is, as it's our fallback language in case no translation can be found