diff --git a/src/constants/languages.ts b/src/constants/languages.ts new file mode 100644 index 00000000..28b06862 --- /dev/null +++ b/src/constants/languages.ts @@ -0,0 +1,11 @@ +import fs from 'fs-extra'; +import path from 'path'; +import { LOCALIZATION_FOLDER } from '@/constants/paths'; + +export const supportedLanguagesMap = JSON.parse(fs.readFileSync(path.join(LOCALIZATION_FOLDER, 'supportedLanguages.json'), 'utf-8')) as Record; +export const tiddlywikiLanguagesMap = JSON.parse(fs.readFileSync(path.join(LOCALIZATION_FOLDER, 'tiddlywikiLanguages.json'), 'utf-8')) as Record< + string, + string | undefined +>; + +export const supportedLanguagesKNames = Object.keys(supportedLanguagesMap); diff --git a/src/pages/Main/index.tsx b/src/pages/Main/index.tsx index fed97656..d8a276bd 100644 --- a/src/pages/Main/index.tsx +++ b/src/pages/Main/index.tsx @@ -27,7 +27,7 @@ import { IWorkspace } from '@services/workspaces/interface'; import { useWorkspacesListObservable } from '@services/workspaces/hooks'; import { usePreferenceObservable } from '@services/preferences/hooks'; import { CommandPaletteIcon } from '@/components/icon/CommandPaletteSVG'; -import { AddWorkspace } from '../AddWorkspace'; +import { Languages } from '../Preferences/sections/Languages'; const OuterRoot = styled.div` display: flex; @@ -386,6 +386,7 @@ export default function Main(): JSX.Element { )} + diff --git a/src/pages/Preferences/sections/Languages.tsx b/src/pages/Preferences/sections/Languages.tsx index 1f4a2ac8..ecb5596a 100644 --- a/src/pages/Preferences/sections/Languages.tsx +++ b/src/pages/Preferences/sections/Languages.tsx @@ -9,57 +9,74 @@ import { WindowNames } from '@services/windows/WindowProperties'; import { hunspellLanguagesMap } from '@/constants/hunspellLanguages'; import { usePromiseValue } from '@/helpers/useServiceValue'; -export function Languages(props: Required): JSX.Element { +export function Languages(props: Partial & { languageSelectorOnly?: boolean }): JSX.Element { const { t } = useTranslation(); const preference = usePreferenceObservable(); - const platform = usePromiseValue(async () => await window.service.context.get('platform')); + const [platform, supportedLanguagesMap]: [string | undefined, Record | undefined] = usePromiseValue( + async (): Promise<[string | undefined, Record | undefined]> => + await Promise.all([window.service.context.get('platform'), window.service.context.get('supportedLanguagesMap')]), + [undefined, undefined], + ); return ( <> - {t('Preference.Languages')} + {t('Preference.Languages')} - {preference === undefined || platform === undefined ? ( + {preference === undefined || platform === undefined || supportedLanguagesMap === undefined || preference.language === undefined ? ( {t('Loading')} ) : ( <> - + - - {t('Preference.Languages')} - - + + {t('Preference.Languages')} + + - - - - - { - await window.service.preference.set('spellcheck', event.target.checked); - props.requestRestartCountDown(); - }} - /> - - - {platform !== 'darwin' && ( + {props.languageSelectorOnly !== true && ( <> - await window.service.window.open(WindowNames.spellcheck)}> - hunspellLanguagesMap[code]).join(' | ')} - /> - + + + + { + await window.service.preference.set('spellcheck', event.target.checked); + props?.requestRestartCountDown?.(); + }} + /> + + {platform !== 'darwin' && ( + <> + + await window.service.window.open(WindowNames.spellcheck)}> + hunspellLanguagesMap[code]).join(' | ')} + /> + + + + )} )} diff --git a/src/services/context/index.ts b/src/services/context/index.ts index f880e263..3579d832 100644 --- a/src/services/context/index.ts +++ b/src/services/context/index.ts @@ -8,6 +8,7 @@ import { injectable } from 'inversify'; import { IContextService, IContext, IPaths, IConstants } from './interface'; import * as paths from '@/constants/paths'; import * as appPaths from '@/constants/appPaths'; +import { tiddlywikiLanguagesMap, supportedLanguagesMap } from '@/constants/languages'; import { getLocalHostUrlWithActualIP } from '@services/libs/url'; @injectable() @@ -20,6 +21,8 @@ export class ContextService implements IContextService { appName: app.name, oSVersion: os.release(), environmentVersions: process.versions, + tiddlywikiLanguagesMap, + supportedLanguagesMap, }; private readonly context: IContext; diff --git a/src/services/context/interface.ts b/src/services/context/interface.ts index 96a0b884..2b95e6f7 100644 --- a/src/services/context/interface.ts +++ b/src/services/context/interface.ts @@ -24,6 +24,8 @@ export interface IConstants { isDevelopment: boolean; oSVersion: string; platform: string; + supportedLanguagesMap: Record; + tiddlywikiLanguagesMap: Record; } export interface IContext extends IPaths, IConstants {} diff --git a/src/services/libs/i18n/requestChangeLanguage.ts b/src/services/libs/i18n/requestChangeLanguage.ts new file mode 100644 index 00000000..62421486 --- /dev/null +++ b/src/services/libs/i18n/requestChangeLanguage.ts @@ -0,0 +1,69 @@ +import { ipcMain } from 'electron'; +import type { IWindowService } from '@services/windows/interface'; +import type { IViewService } from '@services/view/interface'; +import type { IMenuService } from '@services/menu/interface'; +import { I18NChannels, WikiChannel } from '@/constants/channels'; +import { container } from '@services/container'; +import serviceIdentifier from '@services/serviceIdentifier'; +import { tiddlywikiLanguagesMap, supportedLanguagesMap } from '@/constants/languages'; +import { logger } from '../log'; +import i18n from '.'; + +export async function requestChangeLanguage(newLanguage: string): Promise { + const windowService = container.get(serviceIdentifier.Window); + const viewService = container.get(serviceIdentifier.View); + const menuService = container.get(serviceIdentifier.MenuService); + const viewCount = await viewService.getViewCount(); + + await i18n.changeLanguage(newLanguage); + viewService.forEachView((view) => { + view.webContents.send(I18NChannels.changeLanguageRequest, { + lng: newLanguage, + }); + }); + // change tiddlygit language + if (viewCount > 0) { + await windowService.sendToAllWindows(I18NChannels.changeLanguageRequest, { + lng: newLanguage, + }); + } + await Promise.all([ + // change tiddlywiki language + new Promise((resolve) => { + let inProgressCounter = 0; + // we don't wait more than 10s + const twLanguageUpdateTimeout = 10_000; + const tiddlywikiLanguageName = tiddlywikiLanguagesMap[newLanguage]; + if (tiddlywikiLanguageName !== undefined && viewCount > 0) { + const onTimeout = (): void => { + ipcMain.removeListener(WikiChannel.setTiddlerTextDone, onDone); + logger.error( + `When click language menu "${newLanguage}", language "${tiddlywikiLanguageName}" is too slow to update, inProgressCounter is ${inProgressCounter} after ${twLanguageUpdateTimeout}ms.`, + ); + resolve(); + }; + const timeoutHandle = setTimeout(onTimeout, twLanguageUpdateTimeout); + const onDone = (): void => { + inProgressCounter -= 1; + if (inProgressCounter === 0) { + clearTimeout(timeoutHandle); + resolve(); + } + }; + viewService.forEachView((view) => { + inProgressCounter += 1; + ipcMain.once(WikiChannel.setTiddlerTextDone, onDone); + view.webContents.send(WikiChannel.setTiddlerText, '$:/language', tiddlywikiLanguageName); + }); + } else { + logger.error(`When click language menu "${newLanguage}", there is no corresponding tiddlywiki language registered`, { + supportedLanguagesMap, + tiddlywikiLanguagesMap, + }); + resolve(); + } + }), + // update menu + await menuService.buildMenu(), + ]); +} diff --git a/src/services/menu/buildLanguageMenu.ts b/src/services/menu/buildLanguageMenu.ts index fa80b6a5..0b8e90e4 100644 --- a/src/services/menu/buildLanguageMenu.ts +++ b/src/services/menu/buildLanguageMenu.ts @@ -1,85 +1,23 @@ -import fs from 'fs-extra'; -import path from 'path'; - -import type { IWindowService } from '@services/windows/interface'; import serviceIdentifier from '@services/serviceIdentifier'; import { container } from '@services/container'; -import { LOCALIZATION_FOLDER } from '@/constants/paths'; -import { I18NChannels, WikiChannel } from '@/constants/channels'; import type { IPreferenceService } from '@services/preferences/interface'; -import type { IViewService } from '@services/view/interface'; import type { IMenuService, DeferredMenuItemConstructorOptions } from '@services/menu/interface'; -import { ipcMain } from 'electron'; - -const supportedLanguagesMap = JSON.parse(fs.readFileSync(path.join(LOCALIZATION_FOLDER, 'supportedLanguages.json'), 'utf-8')) as Record; -const tiddlywikiLanguagesMap = JSON.parse(fs.readFileSync(path.join(LOCALIZATION_FOLDER, 'tiddlywikiLanguages.json'), 'utf-8')) as Record< - string, - string | undefined ->; - -const supportedLanguagesKNames = Object.keys(supportedLanguagesMap); +import { supportedLanguagesKNames, supportedLanguagesMap } from '@/constants/languages'; /** * Register languages into language menu, call this function after container init */ export function buildLanguageMenu(): void { const preferenceService = container.get(serviceIdentifier.Preference); - const windowService = container.get(serviceIdentifier.Window); - const viewService = container.get(serviceIdentifier.View); + const menuService = container.get(serviceIdentifier.MenuService); const subMenu: DeferredMenuItemConstructorOptions[] = []; for (const language of supportedLanguagesKNames) { subMenu.push({ label: supportedLanguagesMap[language], click: async () => { - const i18n = (await import('../libs/i18n')).default; - const { logger } = await import('../libs/log'); - await Promise.all([preferenceService.set('language', language), i18n.changeLanguage(language)]); - viewService.forEachView((view) => { - view.webContents.send(I18NChannels.changeLanguageRequest, { - lng: language, - }); - }); - // change tiddlygit language - await windowService.sendToAllWindows(I18NChannels.changeLanguageRequest, { - lng: language, - }); - // change tiddlywiki language - await new Promise((resolve) => { - let inProgressCounter = 0; - // we don't wait more than 10s - const twLanguageUpdateTimeout = 10_000; - const tiddlywikiLanguageName = tiddlywikiLanguagesMap[language]; - if (tiddlywikiLanguageName !== undefined) { - const onTimeout = (): void => { - ipcMain.removeListener(WikiChannel.setTiddlerTextDone, onDone); - logger.error( - `When click language menu "${language}", language "${tiddlywikiLanguageName}" is too slow to update, inProgressCounter is ${inProgressCounter} after ${twLanguageUpdateTimeout}ms.`, - ); - }; - const timeoutHandle = setTimeout(onTimeout, twLanguageUpdateTimeout); - const onDone = (): void => { - inProgressCounter -= 1; - if (inProgressCounter === 0) { - clearTimeout(timeoutHandle); - resolve(); - } - }; - viewService.forEachView((view) => { - inProgressCounter += 1; - ipcMain.once(WikiChannel.setTiddlerTextDone, onDone); - view.webContents.send(WikiChannel.setTiddlerText, '$:/language', tiddlywikiLanguageName); - }); - } else { - logger.error(`When click language menu "${language}", there is no corresponding tiddlywiki language registered`, { - supportedLanguagesMap, - tiddlywikiLanguagesMap, - }); - resolve(); - } - }); - await menuService.buildMenu(); + await preferenceService.set('language', language); }, }); } diff --git a/src/services/preferences/index.ts b/src/services/preferences/index.ts index 90515f37..37720c4f 100755 --- a/src/services/preferences/index.ts +++ b/src/services/preferences/index.ts @@ -12,6 +12,7 @@ import i18n from '@services/libs/i18n'; import { IPreferences, IPreferenceService } from './interface'; import { defaultPreferences } from './defaultPreferences'; import { lazyInject } from '@services/container'; +import { requestChangeLanguage } from '@services/libs/i18n/requestChangeLanguage'; @injectable() export class Preference implements IPreferenceService { @@ -80,25 +81,28 @@ export class Preference implements IPreferenceService { this.cachedPreferences[key] = value; this.cachedPreferences = { ...this.cachedPreferences, ...this.sanitizePreference(this.cachedPreferences) }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument await settings.set(`preferences.${key}`, this.cachedPreferences[key] as any); - - this.reactWhenPreferencesChanged(key, value); this.updatePreferenceSubject(); + + await this.reactWhenPreferencesChanged(key, value); } /** * Do some side effect when config change, update other services or filesystem * @param preference new preference settings */ - private reactWhenPreferencesChanged(key: K, value: IPreferences[K]): void { + private async reactWhenPreferencesChanged(key: K, value: IPreferences[K]): Promise { // maybe pauseNotificationsBySchedule or pauseNotifications or ... if (key.startsWith('pauseNotifications')) { - void this.notificationService.updatePauseNotificationsInfo(); + await this.notificationService.updatePauseNotificationsInfo(); } if (key === 'themeSource') { nativeTheme.themeSource = value as IPreferences['themeSource']; } + if (key === 'language') { + await requestChangeLanguage(value as string); + } } /** @@ -106,7 +110,7 @@ export class Preference implements IPreferenceService { */ private async setPreferences(newPreferences: IPreferences): Promise { this.cachedPreferences = newPreferences; - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument await settings.set(`preferences`, { ...newPreferences } as any); this.updatePreferenceSubject(); } diff --git a/src/services/view/index.ts b/src/services/view/index.ts index 2b2655d9..65ba938a 100644 --- a/src/services/view/index.ts +++ b/src/services/view/index.ts @@ -191,6 +191,11 @@ export class View implements IViewService { * Each workspace can have several windows to render its view (main window and menu bar) */ private views: Record | undefined> = {}; + public async getViewCount(): Promise { + // eslint-disable-next-line @typescript-eslint/return-await + return await Promise.resolve(Object.keys(this.views).length); + } + public getView = (workspaceID: string, windowName: WindowNames): BrowserView | undefined => this.views[workspaceID]?.[windowName]; public getAllViewOfWorkspace = (workspaceID: string): BrowserView[] => Object.values(this.views[workspaceID] ?? {}); public setView = (workspaceID: string, windowName: WindowNames, newView: BrowserView): void => { diff --git a/src/services/view/interface.ts b/src/services/view/interface.ts index 3854b577..2dd2f813 100644 --- a/src/services/view/interface.ts +++ b/src/services/view/interface.ts @@ -22,6 +22,7 @@ export interface IViewService { getActiveBrowserViews: () => Promise>; getAllViewOfWorkspace: (workspaceID: string) => BrowserView[]; getView: (workspaceID: string, windowName: WindowNames) => BrowserView | undefined; + getViewCount(): Promise; realignActiveView: (browserWindow: BrowserWindow, activeId: string) => Promise; reloadActiveBrowserView: () => Promise; reloadViewsWebContents(workspaceID?: string | undefined): Promise; @@ -42,6 +43,7 @@ export const ViewServiceIPCDescriptor = { getActiveBrowserView: ProxyPropertyType.Function, getAllViewOfWorkspace: ProxyPropertyType.Function, getView: ProxyPropertyType.Function, + getViewCount: ProxyPropertyType.Function, realignActiveView: ProxyPropertyType.Function, reloadActiveBrowserView: ProxyPropertyType.Function, reloadViewsWebContents: ProxyPropertyType.Function,