diff --git a/packages/frontend/apps/electron-renderer/src/app/app.tsx b/packages/frontend/apps/electron-renderer/src/app/app.tsx index a16f0d612c..25059deb08 100644 --- a/packages/frontend/apps/electron-renderer/src/app/app.tsx +++ b/packages/frontend/apps/electron-renderer/src/app/app.tsx @@ -10,6 +10,7 @@ import { Suspense } from 'react'; import { RouterProvider } from 'react-router-dom'; import { setupEffects } from './effects'; +import { DesktopLanguageSync } from './language-sync'; import { DesktopThemeSync } from './theme-sync'; const { frameworkProvider } = setupEffects(); @@ -46,6 +47,7 @@ export function App() { + } router={router} diff --git a/packages/frontend/apps/electron-renderer/src/app/language-sync.ts b/packages/frontend/apps/electron-renderer/src/app/language-sync.ts new file mode 100644 index 0000000000..c76bd5d5b9 --- /dev/null +++ b/packages/frontend/apps/electron-renderer/src/app/language-sync.ts @@ -0,0 +1,18 @@ +import { DesktopApiService } from '@affine/core/modules/desktop-api'; +import { I18nService } from '@affine/core/modules/i18n'; +import { useLiveData, useService } from '@toeverything/infra'; +import { useEffect } from 'react'; + +export const DesktopLanguageSync = () => { + const i18nService = useService(I18nService); + const currentLanguage = useLiveData(i18nService.i18n.currentLanguageKey$); + const handler = useService(DesktopApiService).api.handler; + + useEffect(() => { + handler.i18n.changeLanguage(currentLanguage ?? 'en').catch(err => { + console.error(err); + }); + }, [currentLanguage, handler]); + + return null; +}; diff --git a/packages/frontend/apps/electron/package.json b/packages/frontend/apps/electron/package.json index 685c0ba184..2fb6221ba7 100644 --- a/packages/frontend/apps/electron/package.json +++ b/packages/frontend/apps/electron/package.json @@ -33,6 +33,7 @@ }, "devDependencies": { "@affine-tools/utils": "workspace:*", + "@affine/i18n": "workspace:*", "@affine/native": "workspace:*", "@affine/nbstore": "workspace:*", "@electron-forge/cli": "^7.6.0", diff --git a/packages/frontend/apps/electron/src/main/handlers.ts b/packages/frontend/apps/electron/src/main/handlers.ts index bc63e4588e..17cb2d7615 100644 --- a/packages/frontend/apps/electron/src/main/handlers.ts +++ b/packages/frontend/apps/electron/src/main/handlers.ts @@ -1,3 +1,4 @@ +import { I18n } from '@affine/i18n'; import { ipcMain } from 'electron'; import { AFFINE_API_CHANNEL_NAME } from '../shared/type'; @@ -21,6 +22,12 @@ export const debugHandlers = { }, }; +export const i18nHandlers = { + changeLanguage: async (_: Electron.IpcMainInvokeEvent, language: string) => { + return I18n.changeLanguage(language); + }, +}; + // Note: all of these handlers will be the single-source-of-truth for the apis exposed to the renderer process export const allHandlers = { debug: debugHandlers, @@ -33,6 +40,7 @@ export const allHandlers = { worker: workerHandlers, recording: recordingHandlers, popup: popupHandlers, + i18n: i18nHandlers, }; export const registerHandlers = () => { diff --git a/packages/frontend/apps/electron/src/main/windows-manager/tab-views.ts b/packages/frontend/apps/electron/src/main/windows-manager/tab-views.ts index be446fbfc8..e9ac9a36c2 100644 --- a/packages/frontend/apps/electron/src/main/windows-manager/tab-views.ts +++ b/packages/frontend/apps/electron/src/main/windows-manager/tab-views.ts @@ -1,5 +1,6 @@ import { join } from 'node:path'; +import { I18n } from '@affine/i18n'; import { app, BrowserWindow, @@ -822,42 +823,53 @@ export class WebContentViewsManager { }, }); - if (spellCheckSettings.enabled) { - view.webContents.on('context-menu', (_event, params) => { - const shouldShow = - params.misspelledWord && params.dictionarySuggestions.length > 0; + view.webContents.on('context-menu', (_event, params) => { + const menu = Menu.buildFromTemplate([ + { + id: 'cut', + label: I18n['com.affine.context-menu.cut'](), + role: 'cut', + enabled: params.editFlags.canCut, + }, + { + id: 'copy', + label: I18n['com.affine.context-menu.copy'](), + role: 'copy', + enabled: params.editFlags.canCopy, + }, + { + id: 'paste', + label: I18n['com.affine.context-menu.paste'](), + role: 'paste', + enabled: params.editFlags.canPaste, + }, + ]); - if (!shouldShow) { - return; - } - const menu = new Menu(); + // Add each spelling suggestion + for (const suggestion of params.dictionarySuggestions) { + menu.append( + new MenuItem({ + label: suggestion, + click: () => view.webContents.replaceMisspelling(suggestion), + }) + ); + } - // Add each spelling suggestion - for (const suggestion of params.dictionarySuggestions) { - menu.append( - new MenuItem({ - label: suggestion, - click: () => view.webContents.replaceMisspelling(suggestion), - }) - ); - } + // Allow users to add the misspelled word to the dictionary + if (params.misspelledWord) { + menu.append( + new MenuItem({ + label: 'Add to dictionary', // TODO: i18n + click: () => + view.webContents.session.addWordToSpellCheckerDictionary( + params.misspelledWord + ), + }) + ); + } - // Allow users to add the misspelled word to the dictionary - if (params.misspelledWord) { - menu.append( - new MenuItem({ - label: 'Add to dictionary', // TODO: i18n - click: () => - view.webContents.session.addWordToSpellCheckerDictionary( - params.misspelledWord - ), - }) - ); - } - - menu.popup(); - }); - } + menu.popup(); + }); this.webViewsMap$.next(this.tabViewsMap.set(viewId, view)); let unsub = () => {}; diff --git a/packages/frontend/apps/electron/tsconfig.json b/packages/frontend/apps/electron/tsconfig.json index aa6b5b48b1..1fad61568a 100644 --- a/packages/frontend/apps/electron/tsconfig.json +++ b/packages/frontend/apps/electron/tsconfig.json @@ -8,6 +8,7 @@ "include": ["./src"], "references": [ { "path": "../../../../tools/utils" }, + { "path": "../../i18n" }, { "path": "../../native" }, { "path": "../../../common/nbstore" }, { "path": "../../../common/infra" } diff --git a/packages/frontend/core/src/modules/app-tabs-header/views/app-tabs-header.tsx b/packages/frontend/core/src/modules/app-tabs-header/views/app-tabs-header.tsx index 0ecfb8b9dd..632b8f780a 100644 --- a/packages/frontend/core/src/modules/app-tabs-header/views/app-tabs-header.tsx +++ b/packages/frontend/core/src/modules/app-tabs-header/views/app-tabs-header.tsx @@ -123,52 +123,57 @@ const WorkbenchView = ({ [tabsHeaderService, workbench.id] ); - const onContextMenu = useAsyncCallback(async () => { - const action = await tabsHeaderService.showContextMenu?.( - workbench.id, - viewIdx - ); - switch (action?.type) { - case 'open-in-split-view': { - track.$.appTabsHeader.$.tabAction({ - control: 'contextMenu', - action: 'openInSplitView', - }); - break; - } - case 'separate-view': { - track.$.appTabsHeader.$.tabAction({ - control: 'contextMenu', - action: 'separateTabs', - }); - break; - } - case 'pin-tab': { - if (action.payload.shouldPin) { + const onContextMenu = useAsyncCallback( + async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + const action = await tabsHeaderService.showContextMenu?.( + workbench.id, + viewIdx + ); + switch (action?.type) { + case 'open-in-split-view': { track.$.appTabsHeader.$.tabAction({ control: 'contextMenu', - action: 'pin', - }); - } else { - track.$.appTabsHeader.$.tabAction({ - control: 'contextMenu', - action: 'unpin', + action: 'openInSplitView', }); + break; } - break; + case 'separate-view': { + track.$.appTabsHeader.$.tabAction({ + control: 'contextMenu', + action: 'separateTabs', + }); + break; + } + case 'pin-tab': { + if (action.payload.shouldPin) { + track.$.appTabsHeader.$.tabAction({ + control: 'contextMenu', + action: 'pin', + }); + } else { + track.$.appTabsHeader.$.tabAction({ + control: 'contextMenu', + action: 'unpin', + }); + } + break; + } + // fixme: when close tab the view may already be gc'ed + case 'close-tab': { + track.$.appTabsHeader.$.tabAction({ + control: 'contextMenu', + action: 'close', + }); + break; + } + default: + break; } - // fixme: when close tab the view may already be gc'ed - case 'close-tab': { - track.$.appTabsHeader.$.tabAction({ - control: 'contextMenu', - action: 'close', - }); - break; - } - default: - break; - } - }, [tabsHeaderService, viewIdx, workbench.id]); + }, + [tabsHeaderService, viewIdx, workbench.id] + ); const contentNode = useMemo(() => { return ( diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index d781ab0158..c74932f441 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -8286,6 +8286,18 @@ export function useAFFiNEI18N(): { * `Copy link` */ ["com.affine.comment.copy-link"](): string; + /** + * `Copy` + */ + ["com.affine.context-menu.copy"](): string; + /** + * `Paste` + */ + ["com.affine.context-menu.paste"](): string; + /** + * `Cut` + */ + ["com.affine.context-menu.cut"](): string; /** * `An internal error occurred.` */ diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 3cdcd6a984..c65c9b90a5 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -2079,6 +2079,9 @@ "com.affine.comment.filter.only-current-mode": "Only current mode", "com.affine.comment.reply": "Reply", "com.affine.comment.copy-link": "Copy link", + "com.affine.context-menu.copy": "Copy", + "com.affine.context-menu.paste": "Paste", + "com.affine.context-menu.cut": "Cut", "error.INTERNAL_SERVER_ERROR": "An internal error occurred.", "error.NETWORK_ERROR": "Network error.", "error.TOO_MANY_REQUEST": "Too many requests.", diff --git a/tools/utils/src/workspace.gen.ts b/tools/utils/src/workspace.gen.ts index eda0f7c360..67ce8cebd5 100644 --- a/tools/utils/src/workspace.gen.ts +++ b/tools/utils/src/workspace.gen.ts @@ -1257,6 +1257,7 @@ export const PackageList = [ name: '@affine/electron', workspaceDependencies: [ 'tools/utils', + 'packages/frontend/i18n', 'packages/frontend/native', 'packages/common/nbstore', 'packages/common/infra', diff --git a/yarn.lock b/yarn.lock index 49aed93a20..8b3facbb59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -552,6 +552,7 @@ __metadata: resolution: "@affine/electron@workspace:packages/frontend/apps/electron" dependencies: "@affine-tools/utils": "workspace:*" + "@affine/i18n": "workspace:*" "@affine/native": "workspace:*" "@affine/nbstore": "workspace:*" "@electron-forge/cli": "npm:^7.6.0"