From a9be08bd4e85e84f1a31a909257791be4e62352a Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 27 Aug 2024 12:07:27 +1000 Subject: [PATCH] feat: add localization to the node side of the app --- ts/mains/main_node.ts | 51 +++++++++++--------- ts/node/locale.ts | 55 +++++++++++---------- ts/node/menu.ts | 72 ++++++++++++++-------------- ts/node/spell_check.ts | 17 +++---- ts/node/sql.ts | 21 +++++--- ts/node/tray_icon.ts | 10 ++-- ts/state/ducks/search.ts | 2 + ts/test/test-utils/utils/stubbing.ts | 7 ++- ts/updater/common.ts | 29 +++++------ ts/updater/index.ts | 15 +++--- ts/updater/updater.ts | 23 ++++----- ts/util/i18n/shared.ts | 14 ++++-- 12 files changed, 174 insertions(+), 142 deletions(-) diff --git a/ts/mains/main_node.ts b/ts/mains/main_node.ts index 2b596810f..db4e97ba9 100644 --- a/ts/mains/main_node.ts +++ b/ts/mains/main_node.ts @@ -159,13 +159,16 @@ if (windowFromUserConfig) { import { readFile } from 'fs-extra'; import { getAppRootPath } from '../node/getRootPath'; import { setLastestRelease } from '../node/latest_desktop_release'; -import { load as loadLocale } from '../node/locale'; import { isDevProd, isTestIntegration } from '../shared/env_vars'; import { classicDark } from '../themes'; +import type { SetupI18nReturnType } from '../types/Localizer'; +import { getTranslationDictionary } from '../util/i18n/translationDictionaries'; +import { getLocale, isLocaleSet, type Locale } from '../util/i18n/shared'; +import { loadLocalizedDictionary } from '../node/locale'; // Both of these will be set after app fires the 'ready' event let logger: Logger | null = null; -let locale: ReturnType; +let i18n: SetupI18nReturnType; function assertLogger(): Logger { if (!logger) { @@ -181,7 +184,7 @@ function prepareURL(pathSegments: Array, moreKeys?: { theme: any }) { slashes: true, query: { name: packageJson.productName, - locale: locale.name, + locale: getLocale(), version: app.getVersion(), commitHash: config.get('commitHash'), environment: (config as any).environment, @@ -343,7 +346,7 @@ async function createWindow() { // Create the browser window. mainWindow = new BrowserWindow(windowOptions); - setupSpellChecker(mainWindow, locale.messages); + setupSpellChecker(mainWindow, i18n); const setWindowFocus = () => { if (!mainWindow) { @@ -492,6 +495,7 @@ ipc.on('set-release-from-file-server', (_event, releaseGotFromFileServer) => { }); let isReadyForUpdates = false; + async function readyForUpdates() { console.log('[updater] isReadyForUpdates', isReadyForUpdates); if (isReadyForUpdates) { @@ -503,12 +507,13 @@ async function readyForUpdates() { // Second, start checking for app updates try { // if the user disabled auto updates, this will actually not start the updater - await updater.start(getMainWindow, userConfig, locale.messages, logger); + await updater.start(getMainWindow, userConfig, i18n, logger); } catch (error) { const log = logger || console; log.error('Error starting update checks:', error && error.stack ? error.stack : error); } } + ipc.once('ready-for-updates', readyForUpdates); // Forcefully call readyForUpdates after 10 minutes. @@ -527,6 +532,7 @@ function openSupportPage() { } let passwordWindow: BrowserWindow | null = null; + async function showPasswordWindow() { if (passwordWindow) { passwordWindow.show(); @@ -614,7 +620,7 @@ async function showAbout() { width: 500, height: 500, resizeable: true, - title: locale.messages.about, + title: i18n('about'), autoHideMenuBar: true, backgroundColor: classicDark['--background-primary-color'], show: false, @@ -710,10 +716,11 @@ app.on('ready', async () => { logger = getLogger(); assertLogger().info('app ready'); assertLogger().info(`starting version ${packageJson.version}`); - if (!locale) { - const appLocale = process.env.LANGUAGE || app.getLocale() || 'en'; - locale = loadLocale({ appLocale, logger }); - assertLogger().info(`locale is ${appLocale}`); + if (!isLocaleSet()) { + const appLocale = (process.env.LANGUAGE || app.getLocale() || 'en') as Locale; + const loadedLocale = loadLocalizedDictionary({ appLocale, logger }); + i18n = loadedLocale.i18n; + assertLogger().info(`locale is ${loadedLocale.locale}`); } const key = getDefaultSQLKey(); @@ -774,7 +781,7 @@ async function showMainWindow(sqlKey: string, passwordAttempt = false) { await sqlNode.initializeSql({ configDir: userDataPath, key: sqlKey, - messages: locale.messages, + i18n, passwordAttempt, }); appStartInitialSpellcheckSetting = await getSpellCheckSetting(); @@ -789,7 +796,7 @@ async function showMainWindow(sqlKey: string, passwordAttempt = false) { await createWindow(); if (getStartInTray().usingTrayIcon) { - tray = createTrayIcon(getMainWindow, locale.messages); + tray = createTrayIcon(getMainWindow, i18n); } setupMenu(); @@ -806,7 +813,7 @@ function setupMenu() { openSupportPage, platform, }; - const template = createTemplate(menuOptions, locale.messages); + const template = createTemplate(menuOptions, i18n); const menu = Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu); } @@ -897,7 +904,10 @@ app.on('web-contents-created', (_createEvent, contents) => { // Ingested in preload.js via a sendSync call ipc.on('locale-data', event => { // eslint-disable-next-line no-param-reassign - event.returnValue = locale.messages; + event.returnValue = { + dictionary: getTranslationDictionary(), + locale: getLocale(), + }; }); ipc.on('draw-attention', () => { @@ -949,8 +959,7 @@ ipc.on('password-window-login', async (event, passPhrase) => { await showMainWindow(passPhrase, passwordAttempt); sendResponse(undefined); } catch (e) { - const localisedError = locale.messages.passwordIncorrect; - sendResponse(localisedError); + sendResponse(i18n('passwordIncorrect')); } }); @@ -959,7 +968,7 @@ ipc.on('start-in-tray-on-start', (event, newValue) => { userConfig.set('startInTray', newValue); if (newValue) { if (!tray) { - tray = createTrayIcon(getMainWindow, locale.messages); + tray = createTrayIcon(getMainWindow, i18n); } } else { // destroy is not working for a lot of desktop env. So for simplicity, we don't destroy it here but just @@ -1011,10 +1020,7 @@ ipc.on('set-password', async (event, passPhrase, oldPhrase) => { const hashMatches = oldPhrase && PasswordUtil.matchesHash(oldPhrase, hash); if (hash && !hashMatches) { - const incorrectOldPassword = locale.messages.passwordCurrentIncorrect; - sendResponse( - incorrectOldPassword || 'Failed to set password: Old password provided is invalid' - ); + sendResponse(i18n('passwordCurrentIncorrect')); return; } @@ -1033,8 +1039,7 @@ ipc.on('set-password', async (event, passPhrase, oldPhrase) => { sendResponse(updatedHash); } } catch (e) { - const localisedError = locale.messages.passwordFailed; - sendResponse(localisedError || 'Failed to set password'); + sendResponse(i18n('passwordFailed')); } }); diff --git a/ts/node/locale.ts b/ts/node/locale.ts index 372c833be..c70b707e4 100644 --- a/ts/node/locale.ts +++ b/ts/node/locale.ts @@ -1,8 +1,10 @@ import fs from 'fs'; -import _ from 'lodash'; import path from 'path'; -import type { LocalizerDictionary } from '../types/Localizer'; +import type { LocalizerDictionary, SetupI18nReturnType } from '../types/Localizer'; import { getAppRootPath } from './getRootPath'; +import type { Locale } from '../util/i18n/shared'; +import { en } from '../localization/locales'; +import { setupI18n } from '../util/i18n/i18n'; function normalizeLocaleName(locale: string) { const dashedLocale = locale.replaceAll('_', '-'); @@ -41,9 +43,12 @@ function getLocaleMessages(locale: string): LocalizerDictionary { return JSON.parse(fs.readFileSync(targetFile, 'utf-8')); } -export function load({ appLocale, logger }: { appLocale?: string; logger?: any } = {}): { - name: string; - messages: LocalizerDictionary; +export function loadLocalizedDictionary({ + appLocale, + logger, +}: { appLocale?: Locale; logger?: any } = {}): { + locale: Locale; + i18n: SetupI18nReturnType; } { if (!appLocale) { throw new TypeError('`appLocale` is required'); @@ -53,29 +58,31 @@ export function load({ appLocale, logger }: { appLocale?: string; logger?: any } throw new TypeError('`logger.error` is required'); } - const english = getLocaleMessages('en'); - const normalizedLocaleName = normalizeLocaleName(appLocale); + // Load locale - if we can't load messages for the current locale, we + // default to 'en' + // + // possible locales: + // https://github.com/electron/electron/blob/master/docs/api/locales.md + let locale = normalizeLocaleName(appLocale) as Locale; + let translationDictionary; try { - // Load locale - if we can't load messages for the current locale, we - // default to 'en' - // - // possible locales: - // https://github.com/electron/electron/blob/master/docs/api/locales.md - - // We start with english, then overwrite that with anything present in locale - const messages = _.merge(english, getLocaleMessages(normalizedLocaleName)); - return { - name: normalizedLocaleName, - messages, - }; + translationDictionary = getLocaleMessages(locale); } catch (e) { - logger.error(`Problem loading messages for locale ${normalizedLocaleName} ${e.stack}`); + logger.error(`Problem loading messages for locale ${locale} ${e.stack}`); logger.error('Falling back to en locale'); - return { - name: 'en', - messages: english, - }; + locale = 'en'; + translationDictionary = en; } + + const i18n = setupI18n({ + locale, + translationDictionary, + }); + + return { + locale, + i18n, + }; } diff --git a/ts/node/menu.ts b/ts/node/menu.ts index b74cea298..e84542ac0 100644 --- a/ts/node/menu.ts +++ b/ts/node/menu.ts @@ -1,5 +1,5 @@ import { isString } from 'lodash'; -import type { LocalizerDictionary } from '../types/Localizer'; +import type { SetupI18nReturnType } from '../types/Localizer'; import { LOCALE_DEFAULTS } from '../localization/constants'; /** @@ -22,7 +22,7 @@ export const createTemplate = ( saveDebugLog: (_event: any, additionalInfo?: string) => void; showWindow: () => void; }, - messages: LocalizerDictionary + i18n: SetupI18nReturnType ) => { if (!isString(options.platform)) { throw new TypeError('`options.platform` must be a string'); @@ -33,81 +33,81 @@ export const createTemplate = ( const template = [ { - label: withAcceleratorPrefix(messages.file), + label: withAcceleratorPrefix(i18n('file')), submenu: [ { type: 'separator', }, { role: 'quit', - label: messages.quit, + label: i18n('quit'), }, ], }, { - label: withAcceleratorPrefix(messages.edit), + label: withAcceleratorPrefix(i18n('edit')), submenu: [ { role: 'undo', - label: messages.undo, + label: i18n('undo'), }, { role: 'redo', - label: messages.redo, + label: i18n('redo'), }, { type: 'separator', }, { role: 'cut', - label: messages.cut, + label: i18n('cut'), }, { role: 'copy', - label: messages.copy, + label: i18n('copy'), }, { role: 'paste', - label: messages.paste, + label: i18n('paste'), }, { role: 'delete', - label: messages.delete, + label: i18n('delete'), }, { role: 'selectall', - label: messages.selectAll, + label: i18n('selectAll'), }, ], }, { - label: withAcceleratorPrefix(messages.view), + label: withAcceleratorPrefix(i18n('view')), submenu: [ { role: 'resetzoom', - label: messages.actualSize, + label: i18n('actualSize'), }, { accelerator: platform === 'darwin' ? 'Command+=' : 'Control+Plus', role: 'zoomin', - label: messages.appearanceZoomIn, + label: i18n('appearanceZoomIn'), }, { role: 'zoomout', - label: messages.appearanceZoomOut, + label: i18n('appearanceZoomOut'), }, { type: 'separator', }, { role: 'togglefullscreen', - label: messages.fullScreenToggle, + label: i18n('fullScreenToggle'), }, { type: 'separator', }, { - label: messages.debugLog, + label: i18n('debugLog'), click: () => { saveDebugLog('save-debug-log'); }, @@ -117,37 +117,37 @@ export const createTemplate = ( }, { role: 'toggledevtools', - label: messages.developerToolsToggle, + label: i18n('developerToolsToggle'), }, ], }, { - label: withAcceleratorPrefix(messages.window), + label: withAcceleratorPrefix(i18n('window')), role: 'window', submenu: [ { role: 'minimize', - label: messages.minimize, + label: i18n('minimize'), }, ], }, { - label: withAcceleratorPrefix(messages.sessionHelp), + label: withAcceleratorPrefix(i18n('sessionHelp')), role: 'help', submenu: [ { - label: messages.updateReleaseNotes, + label: i18n('updateReleaseNotes'), click: openReleaseNotes, }, { - label: messages.supportGoTo, + label: i18n('supportGoTo'), click: openSupportPage, }, { type: 'separator', }, { - label: messages.about, + label: i18n('about'), click: showAbout, }, ], @@ -155,7 +155,7 @@ export const createTemplate = ( ]; if (platform === 'darwin') { - return updateForMac(template, messages, { + return updateForMac(template, i18n, { showAbout, showWindow, }); @@ -166,7 +166,7 @@ export const createTemplate = ( function updateForMac( template: any, - messages: LocalizerDictionary, + i18n: SetupI18nReturnType, options: { showAbout: () => void; showWindow: () => void } ) { const { showAbout, showWindow } = options; @@ -183,7 +183,7 @@ function updateForMac( label: LOCALE_DEFAULTS.app_name, submenu: [ { - label: messages.about, + label: i18n('about'), click: showAbout, }, { @@ -193,22 +193,22 @@ function updateForMac( type: 'separator', }, { - label: messages.hide, + label: i18n('hide'), role: 'hide', }, { - label: messages.hideOthers, + label: i18n('hideOthers'), role: 'hideothers', }, { - label: messages.showAll, + label: i18n('showAll'), role: 'unhide', }, { type: 'separator', }, { - label: messages.quit, + label: i18n('quit'), role: 'quit', }, ], @@ -219,21 +219,21 @@ function updateForMac( // eslint-disable-next-line no-param-reassign template[windowMenuTemplateIndex].submenu = [ { - label: messages.closeWindow, + label: i18n('closeWindow'), accelerator: 'CmdOrCtrl+W', role: 'close', }, { - label: messages.minimize, + label: i18n('minimize'), accelerator: 'CmdOrCtrl+M', role: 'minimize', }, { - label: messages.appearanceZoom, + label: i18n('appearanceZoom'), role: 'zoom', }, { - label: messages.show, + label: i18n('show'), click: showWindow, }, ]; diff --git a/ts/node/spell_check.ts b/ts/node/spell_check.ts index 6368213e7..f09cb0d7c 100644 --- a/ts/node/spell_check.ts +++ b/ts/node/spell_check.ts @@ -1,7 +1,8 @@ -import { BrowserWindow, Menu } from 'electron'; +import { type BrowserWindow, Menu } from 'electron'; import { sync as osLocaleSync } from 'os-locale'; +import type { SetupI18nReturnType } from '../types/Localizer'; -export const setup = (browserWindow: BrowserWindow, messages: any) => { +export const setup = (browserWindow: BrowserWindow, i18n: SetupI18nReturnType) => { const { session } = browserWindow.webContents; const userLocale = process.env.LANGUAGE ? process.env.LANGUAGE @@ -36,7 +37,7 @@ export const setup = (browserWindow: BrowserWindow, messages: any) => { ); } else { template.push({ - label: messages.noSuggestions, + label: i18n('noSuggestions'), enabled: false, }); } @@ -45,30 +46,30 @@ export const setup = (browserWindow: BrowserWindow, messages: any) => { if (params.isEditable) { if (editFlags.canUndo) { - template.push({ label: messages.undo, role: 'undo' }); + template.push({ label: i18n('undo'), role: 'undo' }); } // This is only ever `true` if undo was triggered via the context menu // (not ctrl/cmd+z) if (editFlags.canRedo) { - template.push({ label: messages.redo, role: 'redo' }); + template.push({ label: i18n('redo'), role: 'redo' }); } if (editFlags.canUndo || editFlags.canRedo) { template.push({ type: 'separator' }); } if (editFlags.canCut) { - template.push({ label: messages.cut, role: 'cut' }); + template.push({ label: i18n('cut'), role: 'cut' }); } } if (editFlags.canPaste) { - template.push({ label: messages.paste, role: 'paste' }); + template.push({ label: i18n('paste'), role: 'paste' }); } // Only enable select all in editors because select all in non-editors // results in all the UI being selected if (editFlags.canSelectAll && params.isEditable) { template.push({ - label: messages.selectAll, + label: i18n('selectAll'), role: 'selectall', }); } diff --git a/ts/node/sql.ts b/ts/node/sql.ts index f7244768f..524464bf7 100644 --- a/ts/node/sql.ts +++ b/ts/node/sql.ts @@ -15,6 +15,7 @@ import { intersection, isArray, isEmpty, + isFunction, isNumber, isObject, isString, @@ -47,7 +48,7 @@ import { OPEN_GROUP_ROOMS_V2_TABLE, toSqliteBoolean, } from './database_utility'; -import type { LocalizerDictionary } from '../types/Localizer'; // checked - only node +import type { SetupI18nReturnType } from '../types/Localizer'; // checked - only node import { StorageItem } from './storage_item'; // checked - only node import { @@ -139,12 +140,12 @@ function showFailedToStart() { async function initializeSql({ configDir, key, - messages, + i18n, passwordAttempt, }: { configDir: string; key: string; - messages: LocalizerDictionary; + i18n: SetupI18nReturnType; passwordAttempt: boolean; }) { console.info('initializeSql sqlnode'); @@ -158,8 +159,8 @@ async function initializeSql({ if (!isString(key)) { throw new Error('initialize: key is required!'); } - if (!isObject(messages)) { - throw new Error('initialize: message is required!'); + if (!isFunction(i18n)) { + throw new Error('initialize: i18n is required!'); } _initializePaths(configDir); @@ -209,10 +210,10 @@ async function initializeSql({ } console.log('Database startup error:', error.stack); const button = await dialog.showMessageBox({ - buttons: [messages.errorCopyAndQuit, messages.clearDataAll], + buttons: [i18n('errorCopyAndQuit'), i18n('clearDataAll')], defaultId: 0, detail: redactAll(error.stack), - message: messages.errorDatabase, + message: i18n('errorDatabase'), noLink: true, type: 'error', }); @@ -249,6 +250,7 @@ function removeDB(configDir = null) { // Password hash const PASS_HASH_ID = 'passHash'; + function getPasswordHash() { const item = getItemById(PASS_HASH_ID); return item && item.value; @@ -304,15 +306,18 @@ function updateGuardNodes(nodes: Array) { function createOrUpdateItem(data: StorageItem, instance?: BetterSqlite3.Database) { createOrUpdate(ITEMS_TABLE, data, instance); } + function getItemById(id: string, instance?: BetterSqlite3.Database) { return getById(ITEMS_TABLE, id, instance); } + function getAllItems() { const rows = assertGlobalInstance() .prepare(`SELECT json FROM ${ITEMS_TABLE} ORDER BY id ASC;`) .all(); return map(rows, row => jsonToObject(row.json)); } + function removeItemById(id: string) { removeById(ITEMS_TABLE, id); } @@ -1858,9 +1863,11 @@ function resetAttachmentDownloadPending() { .prepare(`UPDATE ${ATTACHMENT_DOWNLOADS_TABLE} SET pending = 0 WHERE pending != 0;`) .run(); } + function removeAttachmentDownloadJob(id: string) { removeById(ATTACHMENT_DOWNLOADS_TABLE, id); } + function removeAllAttachmentDownloadJobs() { assertGlobalInstance().exec(`DELETE FROM ${ATTACHMENT_DOWNLOADS_TABLE};`); } diff --git a/ts/node/tray_icon.ts b/ts/node/tray_icon.ts index eef25975a..daf29c331 100644 --- a/ts/node/tray_icon.ts +++ b/ts/node/tray_icon.ts @@ -1,7 +1,7 @@ import path from 'path'; -import { app, BrowserWindow, Menu, Tray } from 'electron'; -import { LocalizerDictionary } from '../types/Localizer'; +import { app, type BrowserWindow, Menu, Tray } from 'electron'; +import type { SetupI18nReturnType } from '../types/Localizer'; import { getAppRootPath } from './getRootPath'; import { LOCALE_DEFAULTS } from '../localization/constants'; @@ -11,7 +11,7 @@ let trayAny: any; export function createTrayIcon( getMainWindow: () => BrowserWindow | null, - messages: LocalizerDictionary + i18n: SetupI18nReturnType ) { // keep the duplicated part to allow for search and find const iconFile = process.platform === 'darwin' ? 'session_icon_16.png' : 'session_icon_32.png'; @@ -65,12 +65,12 @@ export function createTrayIcon( trayContextMenu = Menu.buildFromTemplate([ { id: 'toggleWindowVisibility', - label: messages[mainWindow?.isVisible() ? 'hide' : 'show'], + label: mainWindow?.isVisible() ? i18n('hide') : i18n('show'), click: trayAny.toggleWindowVisibility, }, { id: 'quit', - label: messages.quit, + label: i18n('quit'), click: app.quit.bind(app), }, ]); diff --git a/ts/state/ducks/search.ts b/ts/state/ducks/search.ts index c273dc1dd..5e6e1c928 100644 --- a/ts/state/ducks/search.ts +++ b/ts/state/ducks/search.ts @@ -89,12 +89,14 @@ async function doSearch(query: string): Promise { messages: filteredMessages, }; } + export function clearSearch(): ClearSearchActionType { return { type: 'SEARCH_CLEAR', payload: null, }; } + export function updateSearchTerm(query: string): UpdateSearchTermActionType { return { type: 'SEARCH_UPDATE', diff --git a/ts/test/test-utils/utils/stubbing.ts b/ts/test/test-utils/utils/stubbing.ts index 88934385b..7d696bf5b 100644 --- a/ts/test/test-utils/utils/stubbing.ts +++ b/ts/test/test-utils/utils/stubbing.ts @@ -5,10 +5,9 @@ import { ConfigDumpData } from '../../../data/configDump/configDump'; import { Data } from '../../../data/data'; import { OpenGroupData } from '../../../data/opengroups'; -import { load } from '../../../node/locale'; +import { loadLocalizedDictionary } from '../../../node/locale'; import * as libsessionWorker from '../../../webworker/workers/browser/libsession_worker_interface'; import * as utilWorker from '../../../webworker/workers/browser/util_worker_interface'; -import { setupI18n } from '../../../util/i18n/i18n'; const globalAny: any = global; @@ -140,6 +139,6 @@ export async function expectAsyncToThrow(toAwait: () => Promise, errorMessa /** You must call stubWindowLog() before using */ export const stubI18n = () => { - const locale = load({ appLocale: 'en', logger: window.log }); - stubWindow('i18n', setupI18n({ locale: 'en', translationDictionary: locale.messages })); + const { i18n } = loadLocalizedDictionary({ appLocale: 'en', logger: window.log }); + stubWindow('i18n', i18n); }; diff --git a/ts/updater/common.ts b/ts/updater/common.ts index 4218fd7ca..81fe4de3b 100644 --- a/ts/updater/common.ts +++ b/ts/updater/common.ts @@ -1,4 +1,5 @@ import { BrowserWindow, dialog } from 'electron'; +import type { SetupI18nReturnType } from '../types/Localizer'; export type MessagesType = { [key: string]: string; @@ -17,16 +18,16 @@ export type LoggerType = { export async function showDownloadUpdateDialog( mainWindow: BrowserWindow, - messages: MessagesType + i18n: SetupI18nReturnType ): Promise { const DOWNLOAD_BUTTON = 0; const LATER_BUTTON = 1; const options = { type: 'info' as const, - buttons: [messages.download, messages.later], - title: messages.updateSession, - message: messages.updateNewVersionDescription, - detail: messages.updateNewVersionDescription, + buttons: [i18n('download'), i18n('later')], + title: i18n('updateSession'), + message: i18n('updateNewVersionDescription'), + detail: i18n('updateNewVersionDescription'), defaultId: LATER_BUTTON, cancelId: DOWNLOAD_BUTTON, }; @@ -38,16 +39,16 @@ export async function showDownloadUpdateDialog( export async function showUpdateDialog( mainWindow: BrowserWindow, - messages: MessagesType + i18n: SetupI18nReturnType ): Promise { const RESTART_BUTTON = 0; const LATER_BUTTON = 1; const options = { type: 'info' as const, - buttons: [messages.restart, messages.later], - title: messages.updateSession, - message: messages.updateDownloaded, - detail: messages.updateDownloaded, + buttons: [i18n('restart'), i18n('later')], + title: i18n('updateSession'), + message: i18n('updateDownloaded'), + detail: i18n('updateDownloaded'), defaultId: LATER_BUTTON, cancelId: RESTART_BUTTON, }; @@ -56,12 +57,12 @@ export async function showUpdateDialog( return ret.response === RESTART_BUTTON; } -export async function showCannotUpdateDialog(mainWindow: BrowserWindow, messages: MessagesType) { +export async function showCannotUpdateDialog(mainWindow: BrowserWindow, i18n: SetupI18nReturnType) { const options = { type: 'error' as const, - buttons: [messages.ok], - title: messages.updateError, - message: messages.updateErrorDescription, + buttons: [i18n('okay')], + title: i18n('updateError'), + message: i18n('updateErrorDescription'), }; await dialog.showMessageBox(mainWindow, options); } diff --git a/ts/updater/index.ts b/ts/updater/index.ts index 7fd408b8a..0398f8caa 100644 --- a/ts/updater/index.ts +++ b/ts/updater/index.ts @@ -1,7 +1,8 @@ -import { BrowserWindow } from 'electron'; +import type { BrowserWindow } from 'electron'; import { start as startUpdater, stop as stopUpdater } from './updater'; -import { LoggerType, MessagesType } from './common'; -import { UserConfig } from '../node/config/user_config'; +import type { LoggerType } from './common'; +import type { UserConfig } from '../node/config/user_config'; +import type { SetupI18nReturnType } from '../types/Localizer'; let initialized = false; let localUserConfig: UserConfig; @@ -9,7 +10,7 @@ let localUserConfig: UserConfig; export async function start( getMainWindow: () => BrowserWindow | null, userConfig: UserConfig, - messages: MessagesType, + i18n: SetupI18nReturnType, logger?: LoggerType | null ) { if (initialized) { @@ -20,8 +21,8 @@ export async function start( throw new Error('updater/start: userConfig is needed!'); } - if (!messages) { - throw new Error('updater/start: Must provide messages!'); + if (!i18n) { + throw new Error('updater/start: Must provide i18n!'); } if (!logger) { throw new Error('updater/start: Must provide logger!'); @@ -35,7 +36,7 @@ export async function start( return; } - await startUpdater(getMainWindow, messages, logger); + await startUpdater(getMainWindow, i18n, logger); } export function stop() { diff --git a/ts/updater/updater.ts b/ts/updater/updater.ts index 4abe99287..2d00ddde8 100644 --- a/ts/updater/updater.ts +++ b/ts/updater/updater.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-misused-promises */ /* eslint-disable no-console */ import * as path from 'path'; -import { app, BrowserWindow } from 'electron'; -import { autoUpdater, UpdateInfo } from 'electron-updater'; +import { app, type BrowserWindow } from 'electron'; +import { autoUpdater, type UpdateInfo } from 'electron-updater'; import * as fs from 'fs-extra'; import { gt as isVersionGreaterThan, parse as parseVersion } from 'semver'; @@ -11,22 +11,23 @@ import { windowMarkShouldQuit } from '../node/window_state'; import { getLastestRelease } from '../node/latest_desktop_release'; import { getPrintableError, - LoggerType, - MessagesType, + type LoggerType, showCannotUpdateDialog, showDownloadUpdateDialog, showUpdateDialog, } from './common'; +import type { SetupI18nReturnType } from '../types/Localizer'; let isUpdating = false; let downloadIgnored = false; let interval: NodeJS.Timeout | undefined; let stopped = false; + // eslint:disable: no-console export async function start( getMainWindow: () => BrowserWindow | null, - messages: MessagesType, + i18n: SetupI18nReturnType, logger: LoggerType ) { if (interval) { @@ -43,7 +44,7 @@ export async function start( interval = global.setInterval( async () => { try { - await checkForUpdates(getMainWindow, messages, logger); + await checkForUpdates(getMainWindow, i18n, logger); } catch (error) { logger.error('auto-update: error:', getPrintableError(error)); } @@ -55,7 +56,7 @@ export async function start( global.setTimeout( async () => { try { - await checkForUpdates(getMainWindow, messages, logger); + await checkForUpdates(getMainWindow, i18n, logger); } catch (error) { logger.error('auto-update: error:', getPrintableError(error)); } @@ -74,7 +75,7 @@ export function stop() { async function checkForUpdates( getMainWindow: () => BrowserWindow | null, - messages: MessagesType, + i18n: SetupI18nReturnType, logger: LoggerType ) { logger.info('[updater] checkForUpdates'); @@ -141,7 +142,7 @@ async function checkForUpdates( return; } logger.info('[updater] showing download dialog...'); - const shouldDownload = await showDownloadUpdateDialog(mainWindow, messages); + const shouldDownload = await showDownloadUpdateDialog(mainWindow, i18n); logger.info('[updater] shouldDownload:', shouldDownload); if (!shouldDownload) { @@ -157,7 +158,7 @@ async function checkForUpdates( console.error('cannot showDownloadUpdateDialog, mainWindow is unset'); return; } - await showCannotUpdateDialog(mainWindow, messages); + await showCannotUpdateDialog(mainWindow, i18n); throw error; } const window = getMainWindow(); @@ -167,7 +168,7 @@ async function checkForUpdates( } // Update downloaded successfully, we should ask the user to update logger.info('[updater] showing update dialog...'); - const shouldUpdate = await showUpdateDialog(window, messages); + const shouldUpdate = await showUpdateDialog(window, i18n); if (!shouldUpdate) { return; } diff --git a/ts/util/i18n/shared.ts b/ts/util/i18n/shared.ts index 583f511e7..3469b0900 100644 --- a/ts/util/i18n/shared.ts +++ b/ts/util/i18n/shared.ts @@ -9,9 +9,13 @@ let initialLocale: Locale | undefined; * */ export function i18nLog(message: string) { - // eslint:disable: no-console - // eslint-disable-next-line no-console - (window?.log?.info ?? console.log)(`i18n: ${message}`); + if (typeof window !== 'undefined') { + // eslint-disable-next-line no-console + (window?.log?.error ?? console.log)(`i18n: ${message}`); + } else { + // eslint-disable-next-line no-console + console.log(`i18n: ${message}`); + } } export type Locale = keyof typeof timeLocaleMap; @@ -52,3 +56,7 @@ export function getBrowserLocale() { export function setInitialLocale(locale: Locale) { initialLocale = locale; } + +export function isLocaleSet() { + return initialLocale !== undefined; +}