diff --git a/.env.example b/.env.example index 45b35750..b1b45271 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,12 @@ REPO_OWNER=bitfinexcom REPO_BRANCH=master + IS_BFX_API_STAGING=0 IS_DEV_ENV=0 IS_AUTO_UPDATE_DISABLED=0 + +SHOULD_LOCALHOST_BE_USED_FOR_LOADING_UI_IN_DEV_MODE=0 + EP_GH_IGNORE_TIME=true GH_TOKEN= diff --git a/build/locales/en/translations.json b/build/locales/en/translations.json new file mode 100644 index 00000000..081a4a4c --- /dev/null +++ b/build/locales/en/translations.json @@ -0,0 +1,56 @@ +{ + "common": { + "title": "Report" + }, + "menu": { + "macMainSubmenu": { + "servicesLabel": "Services", + "hideLabel": "Hide", + "hideOthersLabel": "Hide Others", + "unhideLabel": "Unhide", + "quitLabel": "Quit" + }, + "fileSubMenu": { + "label": "File" + }, + "editSubMenu": { + "label": "Edit" + }, + "viewSubMenu": { + "label": "View", + "reloadLabel": "Reload", + "forceReloadLabel": "Force Reload", + "toggleDevToolsLabel": "Toggle Developer Tools", + "resetZoomLabel": "Actual Size", + "zoomInLabel": "Zoom In", + "zoomOutLabel": "Zoom Out", + "togglefullscreenLabel": "Toggle Full Screen" + }, + "windowSubMenu": { + "label": "Window" + }, + "toolsSubMenu": { + "label": "Tools", + "dataManagementSubMenu": { + "label": "Data Management", + "exportDbLabel": "Export DB", + "importDbLabel": "Import DB", + "restoreDbLabel": "Restore DB", + "backupDbLabel": "Backup DB", + "removeDbLabel": "Remove DB", + "clearAllDataLabel": "Clear All Data (except user creds)" + }, + "changeReportsFolderLabel": "Change Reports Folder", + "changeSyncFrequencyLabel": "Change Sync Frequency" + }, + "helpSubMenu": { + "label": "Help", + "openNewGitHubIssueLabel": "Open new GitHub Issue", + "checkForUpdatesLabel": "Check for Updates", + "quitAndInstallUpdatesLabel": "Quit and Install updates", + "userManualLabel": "User Manual", + "changelogLabel": "Changelog", + "aboutLabel": "About {{appName}}" + } + } +} diff --git a/build/locales/es-EM/translations.json b/build/locales/es-EM/translations.json new file mode 100644 index 00000000..a2b9a955 --- /dev/null +++ b/build/locales/es-EM/translations.json @@ -0,0 +1,5 @@ +{ + "common": { + "title": "Reporte" + } +} diff --git a/build/locales/pt-BR/translations.json b/build/locales/pt-BR/translations.json new file mode 100644 index 00000000..e9d0799e --- /dev/null +++ b/build/locales/pt-BR/translations.json @@ -0,0 +1,5 @@ +{ + "common": { + "title": "Informes" + } +} diff --git a/build/locales/ru/translations.json b/build/locales/ru/translations.json new file mode 100644 index 00000000..df5ac1aa --- /dev/null +++ b/build/locales/ru/translations.json @@ -0,0 +1,56 @@ +{ + "common": { + "title": "Отчет" + }, + "menu": { + "macMainSubmenu": { + "servicesLabel": "Услуги", + "hideLabel": "Скрыть", + "hideOthersLabel": "Скрыть Другие", + "unhideLabel": "Показать", + "quitLabel": "Выйти" + }, + "fileSubMenu": { + "label": "Файл" + }, + "editSubMenu": { + "label": "Редактировать" + }, + "viewSubMenu": { + "label": "Вид", + "reloadLabel": "Перезагрузить", + "forceReloadLabel": "Принудительно Перезагрузить", + "toggleDevToolsLabel": "Переключить Инструменты Разработчика", + "resetZoomLabel": "Фактический Размер", + "zoomInLabel": "Увеличить", + "zoomOutLabel": "Уменьшить", + "togglefullscreenLabel": "Переключить на Полный Экран" + }, + "windowSubMenu": { + "label": "Окно" + }, + "toolsSubMenu": { + "label": "Инструменты", + "dataManagementSubMenu": { + "label": "Управление Данными", + "exportDbLabel": "Экспортировать БД", + "importDbLabel": "Импортировать БД", + "restoreDbLabel": "Восстановить БД", + "backupDbLabel": "Резервная копия БД", + "removeDbLabel": "Удалить БД", + "clearAllDataLabel": "Очистить Все Данные (кроме учетных данных польз.)" + }, + "changeReportsFolderLabel": "Изменить Папку Отчетов", + "changeSyncFrequencyLabel": "Изменить Частоту Синхронизации" + }, + "helpSubMenu": { + "label": "Помощь", + "openNewGitHubIssueLabel": "Открыть новую Проблему на GitHub", + "checkForUpdatesLabel": "Проверить наличие Обновлений", + "quitAndInstallUpdatesLabel": "Выйти и Установить обновления", + "userManualLabel": "Руководство Пользователя", + "changelogLabel": "Журнал Изменений", + "aboutLabel": "О {{appName}}" + } + } +} diff --git a/build/locales/tr/translations.json b/build/locales/tr/translations.json new file mode 100644 index 00000000..3af86590 --- /dev/null +++ b/build/locales/tr/translations.json @@ -0,0 +1,5 @@ +{ + "common": { + "title": "Rapor" + } +} diff --git a/build/locales/vi/translations.json b/build/locales/vi/translations.json new file mode 100644 index 00000000..2fd4a0ec --- /dev/null +++ b/build/locales/vi/translations.json @@ -0,0 +1,5 @@ +{ + "common": { + "title": "Báo cáo" + } +} diff --git a/build/locales/zh-CN/translations.json b/build/locales/zh-CN/translations.json new file mode 100644 index 00000000..a62639e0 --- /dev/null +++ b/build/locales/zh-CN/translations.json @@ -0,0 +1,5 @@ +{ + "common": { + "title": "报告" + } +} diff --git a/build/locales/zh-TW/translations.json b/build/locales/zh-TW/translations.json new file mode 100644 index 00000000..90048519 --- /dev/null +++ b/build/locales/zh-TW/translations.json @@ -0,0 +1,5 @@ +{ + "common": { + "title": "報告" + } +} diff --git a/electron-builder-config.js b/electron-builder-config.js index c3429703..3cc0a43a 100644 --- a/electron-builder-config.js +++ b/electron-builder-config.js @@ -185,6 +185,7 @@ module.exports = { files: [ '**/*', 'build/icons', + 'build/locales', 'build/icon.*', 'build/loader.*', '!scripts${/*}', diff --git a/index.js b/index.js index 4de1e892..ef0a267b 100644 --- a/index.js +++ b/index.js @@ -13,6 +13,8 @@ try { } catch (err) {} const { app } = require('electron') +require('./src/i18next') + .initI18next() const isTestEnv = process.env.NODE_ENV === 'test' diff --git a/package.json b/package.json index faa228cf..ad5d218f 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "get-port": "7.0.0", "github-markdown-css": "5.1.0", "grenache-grape": "git+https://github.com/bitfinexcom/grenache-grape.git", + "i18next": "23.15.1", + "i18next-fs-backend": "2.3.2", "js-yaml": "4.1.0", "lib-js-util-base": "git+https://github.com/bitfinexcom/lib-js-util-base.git", "new-github-issue-url": "0.2.1", @@ -40,6 +42,7 @@ "@wdio/mocha-framework": "8.22.0", "@wdio/spec-reporter": "8.21.0", "app-builder-bin": "4.2.0", + "concurrently": "9.0.1", "cross-env": "7.0.3", "dotenv": "16.3.1", "electron": "27.3.5", @@ -58,11 +61,13 @@ }, "scripts": { "start": "cross-env NODE_ENV=development DEBUG=* electron .", + "startWithUIOnPort": "concurrently -ki -c green,blue -n UI,ELECTRON \"npm run startUI\" \"cross-env NODE_ENV=development DEBUG=* SHOULD_LOCALHOST_BE_USED_FOR_LOADING_UI_IN_DEV_MODE=1 electron .\"", "test": "standard && npm run unit", "unit": "cross-env NODE_ENV=test mocha './src/**/__test__/*.spec.js' --config .mocharc.json", "setup": "./scripts/setup.sh", "launch": "./scripts/launch.sh", "sync-repo": "./scripts/sync-repo.sh", - "e2e": "cross-env NODE_ENV=test wdio run ./wdio.conf.js" + "e2e": "cross-env NODE_ENV=test wdio run ./wdio.conf.js", + "startUI": "cd bfx-report-ui && cross-env BROWSER=none npm start" } } diff --git a/src/create-menu/index.js b/src/create-menu/index.js index 6537b876..6601db65 100644 --- a/src/create-menu/index.js +++ b/src/create-menu/index.js @@ -1,6 +1,7 @@ 'use strict' const electron = require('electron') +const i18next = require('i18next') const { app, Menu } = electron const isMac = process.platform === 'darwin' @@ -27,37 +28,99 @@ const MENU_ITEM_IDS = require('./menu.item.ids') const isAutoUpdateDisabled = parseEnvValToBool(process.env.IS_AUTO_UPDATE_DISABLED) -module.exports = ({ - pathToUserData, - pathToUserDocuments -}) => { +let pathToUserData = null +let pathToUserDocuments = null +let isMenuInitialized = false + +const _getPrevMenuItemPropsById = (id, params) => { + const paramsArr = Array.isArray(params) + ? params + : [params] + const res = {} + + for (const opts of paramsArr) { + const { + propName, + defaultVal = true + } = opts ?? {} + + if (!propName) { + continue + } + if ( + !id || + !isMenuInitialized + ) { + res[propName] = defaultVal + + continue + } + + const prevMenu = Menu.getApplicationMenu() + const menuItem = prevMenu?.getMenuItemById?.(id) + + res[propName] = menuItem?.[propName] ?? defaultVal + } + + return res +} + +module.exports = (params) => { + pathToUserData = params?.pathToUserData ?? pathToUserData + pathToUserDocuments = params?.pathToUserDocuments ?? pathToUserDocuments + + if ( + !pathToUserData || + !pathToUserDocuments + ) { + return + } + const menuTemplate = [ ...(isMac ? [{ label: app.name, submenu: [ { - label: `About ${app.name}`, + label: i18next.t( + 'menu.helpSubMenu.aboutLabel', + { appName: app.name } + ), click: showAboutModalDialog() }, { type: 'separator' }, - { role: 'services' }, + { + role: 'services', + label: i18next.t('menu.macMainSubmenu.servicesLabel') + }, { type: 'separator' }, - { role: 'hide' }, - { role: 'hideOthers' }, - { role: 'unhide' }, + { + role: 'hide', + label: i18next.t('menu.macMainSubmenu.hideLabel') + }, + { + role: 'hideOthers', + label: i18next.t('menu.macMainSubmenu.hideOthersLabel') + }, + { + role: 'unhide', + label: i18next.t('menu.macMainSubmenu.unhideLabel') + }, { type: 'separator' }, - { role: 'quit' } + { + role: 'quit', + label: i18next.t('menu.macMainSubmenu.quitLabel') + } ] }] : []), - { role: 'fileMenu' }, - { role: 'editMenu' }, + { role: 'fileMenu', label: i18next.t('menu.fileSubMenu.label') }, + { role: 'editMenu', label: i18next.t('menu.editSubMenu.label') }, { - label: 'View', + label: i18next.t('menu.viewSubMenu.label'), submenu: [ { - label: 'Reload', + label: i18next.t('menu.viewSubMenu.reloadLabel'), accelerator: 'CmdOrCtrl+R', click: (item, focusedWindow) => { if (focusedWindow) { @@ -68,7 +131,7 @@ module.exports = ({ } }, { - label: 'Force Reload', + label: i18next.t('menu.viewSubMenu.forceReloadLabel'), accelerator: 'CmdOrCtrl+Shift+R', click: (item, focusedWindow) => { if (focusedWindow) { @@ -78,44 +141,59 @@ module.exports = ({ triggerElectronLoad() } }, - { role: 'toggleDevTools' }, + { + role: 'toggleDevTools', + label: i18next.t('menu.viewSubMenu.toggleDevToolsLabel') + }, { type: 'separator' }, - { role: 'resetZoom' }, - { role: 'zoomIn' }, - { role: 'zoomOut' }, + { + role: 'resetZoom', + label: i18next.t('menu.viewSubMenu.resetZoomLabel') + }, + { + role: 'zoomIn', + label: i18next.t('menu.viewSubMenu.zoomInLabel') + }, + { + role: 'zoomOut', + label: i18next.t('menu.viewSubMenu.zoomOutLabel') + }, { type: 'separator' }, - { role: 'togglefullscreen' } + { + role: 'togglefullscreen', + label: i18next.t('menu.viewSubMenu.togglefullscreenLabel') + } ] }, - { role: 'windowMenu' }, + { role: 'windowMenu', label: i18next.t('menu.windowSubMenu.label') }, { - label: 'Tools', + label: i18next.t('menu.toolsSubMenu.label'), submenu: [ { - label: 'Data Management', + label: i18next.t('menu.toolsSubMenu.dataManagementSubMenu.label'), submenu: [ { - label: 'Export DB', + label: i18next.t('menu.toolsSubMenu.dataManagementSubMenu.exportDbLabel'), click: exportDB({ pathToUserData, pathToUserDocuments }) }, { - label: 'Import DB', + label: i18next.t('menu.toolsSubMenu.dataManagementSubMenu.importDbLabel'), click: importDB({ pathToUserData, pathToUserDocuments }) }, { - label: 'Restore DB', + label: i18next.t('menu.toolsSubMenu.dataManagementSubMenu.restoreDbLabel'), click: restoreDB() }, { - label: 'Backup DB', + label: i18next.t('menu.toolsSubMenu.dataManagementSubMenu.backupDbLabel'), click: backupDB() }, { - label: 'Remove DB', + label: i18next.t('menu.toolsSubMenu.dataManagementSubMenu.removeDbLabel'), click: removeDB({ pathToUserData }) }, { - label: 'Clear all data', + label: i18next.t('menu.toolsSubMenu.dataManagementSubMenu.clearAllDataLabel'), click: removeDB({ pathToUserData, shouldAllTablesBeCleared: true @@ -125,53 +203,71 @@ module.exports = ({ }, { type: 'separator' }, { - label: 'Change reports folder', + label: i18next.t('menu.toolsSubMenu.changeReportsFolderLabel'), click: changeReportsFolder({ pathToUserDocuments }) }, { - label: 'Change sync frequency', + label: i18next.t('menu.toolsSubMenu.changeSyncFrequencyLabel'), click: changeSyncFrequency() } ] }, { role: 'help', + label: i18next.t('menu.helpSubMenu.label'), submenu: [ { - label: 'Open new GitHub issue', + label: i18next.t('menu.helpSubMenu.openNewGitHubIssueLabel'), id: MENU_ITEM_IDS.REPORT_BUG_MENU_ITEM, - click: manageNewGithubIssue + click: manageNewGithubIssue, + ..._getPrevMenuItemPropsById(MENU_ITEM_IDS.REPORT_BUG_MENU_ITEM, [ + { propName: 'visible', defaultVal: true }, + { propName: 'enabled', defaultVal: true } + ]) }, { type: 'separator' }, { - label: 'Check for updates', - enabled: !isAutoUpdateDisabled, + label: i18next.t('menu.helpSubMenu.checkForUpdatesLabel'), id: MENU_ITEM_IDS.CHECK_UPDATE_MENU_ITEM, - click: checkForUpdates() + click: checkForUpdates(), + ..._getPrevMenuItemPropsById(MENU_ITEM_IDS.CHECK_UPDATE_MENU_ITEM, [ + { propName: 'visible', defaultVal: true }, + { propName: 'enabled', defaultVal: !isAutoUpdateDisabled } + ]) }, { - label: 'Quit and install updates', - visible: false, + label: i18next.t('menu.helpSubMenu.quitAndInstallUpdatesLabel'), id: MENU_ITEM_IDS.INSTALL_UPDATE_MENU_ITEM, - click: quitAndInstall() + click: quitAndInstall(), + ..._getPrevMenuItemPropsById(MENU_ITEM_IDS.INSTALL_UPDATE_MENU_ITEM, [ + { propName: 'visible', defaultVal: false }, + { propName: 'enabled', defaultVal: true } + ]) }, { type: 'separator' }, { - label: 'User manual', + label: i18next.t('menu.helpSubMenu.userManualLabel'), accelerator: 'CmdOrCtrl+H', click: () => showDocs() }, { - label: 'Changelog', + label: i18next.t('menu.helpSubMenu.changelogLabel'), id: MENU_ITEM_IDS.SHOW_CHANGE_LOG_MENU_ITEM, - click: () => showChangelog() + click: () => showChangelog(), + ..._getPrevMenuItemPropsById(MENU_ITEM_IDS.SHOW_CHANGE_LOG_MENU_ITEM, [ + { propName: 'visible', defaultVal: true }, + { propName: 'enabled', defaultVal: true } + ]) }, ...(isMac ? [] : [ { type: 'separator' }, { - label: 'About', + label: i18next.t( + 'menu.helpSubMenu.aboutLabel', + { appName: app.name } + ), click: showAboutModalDialog() } ]) @@ -180,4 +276,5 @@ module.exports = ({ ] Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate)) + isMenuInitialized = true } diff --git a/src/helpers/index.js b/src/helpers/index.js index ef19af8b..964742ad 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -16,6 +16,7 @@ const productName = require('./product-name') const getAlertCustomClassObj = require('./get-alert-custom-class-obj') const parseEnvValToBool = require('./parse-env-val-to-bool') const isBfxApiStaging = require('./is-bfx-api-staging') +const waitPort = require('./wait-port') module.exports = { getFreePort, @@ -27,5 +28,6 @@ module.exports = { productName, getAlertCustomClassObj, parseEnvValToBool, - isBfxApiStaging + isBfxApiStaging, + waitPort } diff --git a/src/helpers/wait-port.js b/src/helpers/wait-port.js new file mode 100644 index 00000000..89bc4e35 --- /dev/null +++ b/src/helpers/wait-port.js @@ -0,0 +1,41 @@ +'use strict' + +const net = require('net') + +module.exports = (args) => { + const { host, port } = args ?? {} + + const client = new net.Socket() + let isStarted = false + let errCount = 0 + + return new Promise((resolve, reject) => { + const listener = () => { + client.end() + + if (isStarted) return + + isStarted = true + resolve() + } + const tryConnection = () => { + client.removeListener('connect', listener) + client.connect({ host, port }, listener) + } + + client.on('error', (err) => { + errCount += 1 + + if (errCount >= 60) { + client.end() + reject(err) + + return + } + + setTimeout(tryConnection, 1000) + }) + + tryConnection() + }) +} diff --git a/src/i18next/index.js b/src/i18next/index.js new file mode 100644 index 00000000..d7e2d305 --- /dev/null +++ b/src/i18next/index.js @@ -0,0 +1,102 @@ +'use strict' + +const { app } = require('electron') +const i18next = require('i18next') +const Backend = require('i18next-fs-backend') +const path = require('path') +const fs = require('fs') +const { rootPath } = require('electron-root-path') + +const transPath = path.join(rootPath, 'build/locales') +const allFileNames = fs.readdirSync(transPath) +const availableLanguages = [...allFileNames.reduce((accum, fileName) => { + const filePath = path.join(transPath, fileName) + const stats = fs.lstatSync(filePath) + + if (stats.isDirectory()) { + accum.add(fileName) + } + + return accum +}, new Set())] + +let i18nextInstance = null + +const _getLanguageFromAvailableOnes = (language) => { + const lngs = getAvailableLanguages() + + if (lngs.some((lng) => lng === language)) { + return language + } + + const lng = lngs.find((lng) => ( + lng.startsWith(language) || + language.startsWith(lng) + )) + + if (lng) { + return lng + } + + const normalizedLng = language.replace(/-\S*/, '') + + return lngs.find((lng) => lng.startsWith(normalizedLng)) +} + +const _getDefaultLanguage = () => { + const defaultLanguages = [ + ...app.getPreferredSystemLanguages(), + app.getLocale(), + 'en' + ] + + for (const defaultLanguage of defaultLanguages) { + const availableLanguage = _getLanguageFromAvailableOnes(defaultLanguage) + + if ( + availableLanguage && + typeof availableLanguage === 'string' + ) { + return availableLanguage + } + } + + return 'en' +} + +const initI18next = () => { + if (i18nextInstance) { + return i18nextInstance + } + + const configs = { + initImmediate: false, + fallbackLng: { + es: ['es-EM'], + pt: ['pt-BR'], + zh: ['zh-CN'], + default: ['en'] + }, + lng: _getDefaultLanguage(), + ns: ['translations'], + defaultNS: 'translations', + preload: availableLanguages, + backend: { + loadPath: path.join(transPath, '{{lng}}/{{ns}}.json') + } + } + + i18next + .use(Backend) + .init(configs) + i18nextInstance = i18next + + return i18next +} + +const getAvailableLanguages = () => availableLanguages + +module.exports = { + getAvailableLanguages, + initI18next +} diff --git a/src/initialize-app.js b/src/initialize-app.js index 85710b5a..942567c8 100644 --- a/src/initialize-app.js +++ b/src/initialize-app.js @@ -2,9 +2,13 @@ const { app } = require('electron') const path = require('path') +const i18next = require('i18next') const { REPORT_FILES_PATH_VERSION } = require('./const') +const TranslationIpcChannelHandlers = require( + './window-creators/main-renderer-ipc-bridge/translation-ipc-channel-handlers' +) const triggerSyncAfterUpdates = require('./trigger-sync-after-updates') const triggerElectronLoad = require('./trigger-electron-load') const wins = require('./window-creators/windows') @@ -136,6 +140,7 @@ const _manageConfigs = (params = {}) => { const configsKeeper = configsKeeperFactory( { pathToUserData }, { + language: null, pathToUserReportFiles, schedulerRule, shownChangelogVer: '0.0.0', @@ -146,6 +151,8 @@ const _manageConfigs = (params = {}) => { configsKeeper, { pathToUserReportFiles } ) + + return configsKeeper } module.exports = async () => { @@ -165,10 +172,17 @@ module.exports = async () => { const pathToUserData = app.getPath('userData') const pathToUserDocuments = app.getPath('documents') - _manageConfigs({ + const configsKeeper = _manageConfigs({ pathToUserData, pathToUserDocuments }) + const savedLanguage = configsKeeper.getConfigByName('language') + + if (savedLanguage) { + await i18next.changeLanguage(savedLanguage) + } + + TranslationIpcChannelHandlers.create() const secretKey = await makeOrReadSecretKey( { pathToUserData } diff --git a/src/window-creators/index.js b/src/window-creators/index.js index c9bddda1..86559669 100644 --- a/src/window-creators/index.js +++ b/src/window-creators/index.js @@ -23,7 +23,16 @@ const { hideWindow, centerWindow } = require('../helpers/manage-window') -const isBfxApiStaging = require('../helpers/is-bfx-api-staging') +const { + isBfxApiStaging, + parseEnvValToBool, + waitPort +} = require('../helpers') + +const shouldLocalhostBeUsedForLoadingUIInDevMode = parseEnvValToBool( + process.env.SHOULD_LOCALHOST_BE_USED_FOR_LOADING_UI_IN_DEV_MODE +) +const uiPort = process.env.UI_PORT ?? 3000 const publicDir = path.join(__dirname, '../../bfx-report-ui/build') const loadURL = serve({ directory: publicDir }) @@ -47,6 +56,29 @@ const _getFileURL = (params) => { return fileURL.toString() } +const _loadUI = async (params) => { + const { + winName, + pathname + } = params ?? {} + + if ( + !pathname && + isDevEnv && + shouldLocalhostBeUsedForLoadingUIInDevMode + ) { + const uiHost = 'localhost' + await waitPort({ host: uiHost, port: uiPort }) + + return wins[winName].loadURL(`http://${uiHost}:${uiPort}`) + } + if (pathname) { + return wins[winName].loadURL(_getFileURL({ pathname })) + } + + return loadURL(wins[winName]) +} + const _createWindow = async ( { pathname = null, @@ -96,7 +128,7 @@ const _createWindow = async ( ...props, webPreferences: { - preload: path.join(__dirname, 'preload.js'), + preload: path.join(__dirname, 'main-renderer-ipc-bridge/preload.js'), ...props?.webPreferences } } @@ -117,9 +149,7 @@ const _createWindow = async ( const isReadyToShowPromise = new Promise((resolve) => { wins[winName].once('ready-to-show', resolve) }) - const didFinishLoadPromise = pathname - ? wins[winName].loadURL(_getFileURL({ pathname })) - : loadURL(wins[winName]) + const didFinishLoadPromise = _loadUI({ winName, pathname }) await Promise.all([ isReadyToShowPromise, diff --git a/src/window-creators/main-renderer-ipc-bridge/ipc.channel.handlers.js b/src/window-creators/main-renderer-ipc-bridge/ipc.channel.handlers.js new file mode 100644 index 00000000..ad99fe0c --- /dev/null +++ b/src/window-creators/main-renderer-ipc-bridge/ipc.channel.handlers.js @@ -0,0 +1,45 @@ +'use strict' + +const { ipcMain } = require('electron') + +class IpcChannelHandlers { + constructor (channelName) { + this.channelName = channelName ?? 'general' + + this.#setup() + } + + static create () { + return new this() + } + + #setup () { + const methodNames = Object.getOwnPropertyNames( + Object.getPrototypeOf(this) + ) + methodNames.shift() + + for (const handlerName of methodNames) { + if (!handlerName.endsWith('Handler')) { + continue + } + + const methodName = this.#getMethodName(handlerName) + const eventName = this.#getEventName(methodName) + + ipcMain.handle(eventName, (event, args) => { + return this[handlerName](event, args) + }) + } + } + + #getMethodName (handlerName) { + return handlerName.replace(/Handler$/, '') + } + + #getEventName (method) { + return `${this.channelName}:${method}` + } +} + +module.exports = IpcChannelHandlers diff --git a/src/window-creators/main-renderer-ipc-bridge/preload.js b/src/window-creators/main-renderer-ipc-bridge/preload.js new file mode 100644 index 00000000..6975813e --- /dev/null +++ b/src/window-creators/main-renderer-ipc-bridge/preload.js @@ -0,0 +1,41 @@ +'use strict' + +const { contextBridge, ipcRenderer } = require('electron') +const isTestEnv = process.env.NODE_ENV === 'test' + +const CHANNEL_NAMES = { + TRANSLATIONS: 'translations' +} + +const INVOKE_METHOD_NAMES = { + SET_LANGUAGE: 'setLanguage', + GET_LANGUAGE: 'getLanguage', + GET_AVAILABLE_LANGUAGES: 'getAvailableLanguages' +} + +const getEventName = (channel, method) => { + return `${channel}:${method}` +} + +const invoke = (channel, method, args) => { + const eventName = getEventName(channel, method) + + return ipcRenderer.invoke(eventName, args) +} + +const bfxReportElectronApi = {} + +for (const methodName of Object.values(INVOKE_METHOD_NAMES)) { + bfxReportElectronApi[methodName] = (args) => { + return invoke(CHANNEL_NAMES.TRANSLATIONS, methodName, args) + } +} + +if (isTestEnv) { + require('wdio-electron-service/preload') +} + +contextBridge.exposeInMainWorld( + 'bfxReportElectronApi', + bfxReportElectronApi +) diff --git a/src/window-creators/main-renderer-ipc-bridge/translation-ipc-channel-handlers.js b/src/window-creators/main-renderer-ipc-bridge/translation-ipc-channel-handlers.js new file mode 100644 index 00000000..6a329627 --- /dev/null +++ b/src/window-creators/main-renderer-ipc-bridge/translation-ipc-channel-handlers.js @@ -0,0 +1,54 @@ +'use strict' + +const i18next = require('i18next') + +const IpcChannelHandlers = require('./ipc.channel.handlers') +const { getConfigsKeeperByName } = require('../../configs-keeper') +const { getAvailableLanguages } = require('../../i18next') +const createMenu = require('../../create-menu') + +class TranslationIpcChannelHandlers extends IpcChannelHandlers { + constructor () { + super('translations') + + this.configsKeeper = getConfigsKeeperByName('main') + } + + async setLanguageHandler (event, args) { + const lng = args?.language + + if ( + !lng || + typeof lng !== 'string' + ) { + return false + } + + const prevLanguage = i18next.resolvedLanguage + await i18next.changeLanguage(lng) + const language = i18next.resolvedLanguage + + if (prevLanguage !== language) { + createMenu() + } + + const isSaved = await this.configsKeeper + .saveConfigs({ language }) + + if (isSaved) { + return language + } + + return false + } + + async getLanguageHandler (event, args) { + return i18next.resolvedLanguage + } + + async getAvailableLanguagesHandler (event, args) { + return getAvailableLanguages() + } +} + +module.exports = TranslationIpcChannelHandlers diff --git a/src/window-creators/preload.js b/src/window-creators/preload.js deleted file mode 100644 index 804b55ab..00000000 --- a/src/window-creators/preload.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict' - -const { ipcRenderer, contextBridge } = require('electron') - -// See the Electron documentation for details on how to use preload scripts: -// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts -const isTest = process.env.NODE_ENV === 'test' - -if (isTest) { - require('wdio-electron-service/preload') -} - -contextBridge.exposeInMainWorld('electron', { - openDialog: (method, config) => ipcRenderer - .send('dialog', method, config) -})