diff --git a/main/common/localization.ts b/main/common/localization.ts new file mode 100644 index 00000000..439c7ab4 --- /dev/null +++ b/main/common/localization.ts @@ -0,0 +1,52 @@ +import {settings} from './settings'; + +const simplifiedChinese: Record = { + 'Preferences…': '偏好设置…', + 'About Kap': '关于 Kap', + 'Open Video…': '打开视频…', + Videos: '视频', + 'Export History': '导出历史', + 'Send Feedback…': '发送反馈…', + Help: '帮助', + File: '文件', + Edit: '编辑', + Window: '窗口', + Plugins: '插件', + Microphone: '麦克风', + None: '无', + 'System Default': '系统默认', + Stop: '停止', + Pause: '暂停', + Resume: '继续', + Preferences: '偏好设置', + Exports: '导出记录', + 'Save Original…': '保存原文件…', + Discard: '丢弃', + Close: '关闭', + Minimize: '最小化', + Zoom: '缩放', + 'Bring All to Front': '全部置于顶层', + 'Open System Preferences': '打开系统设置', + Cancel: '取消', + Continue: '继续', + 'Are you sure that you want to discard this recording?': '确定要丢弃此录制吗?', + 'You will no longer be able to edit and export the original recording.': '丢弃后将无法再编辑或导出原始录制。', + 'Kap cannot access the microphone.': 'Kap 无法访问麦克风。', + 'Kap requires microphone access to be able to record audio. You can grant this in the System Preferences. Afterwards, launch Kap for the changes to take effect.': 'Kap 需要麦克风权限才能录制音频。请在系统设置中授予权限,然后重新启动 Kap。', + 'Kap cannot record the screen.': 'Kap 无法录制屏幕。', + 'Kap requires screen capture access to be able to record the screen. You can grant this in the System Preferences. Afterwards, launch Kap for the changes to take effect.': 'Kap 需要屏幕录制权限。请在系统设置中授予权限,然后重新启动 Kap。', + 'Audio recording is enabled but Kap does not have access to the microphone. Continue without audio or grant Kap access to the microphone the System Preferences.': '已启用音频录制,但 Kap 没有麦克风权限。你可以继续进行无声录制,或前往系统设置授予麦克风权限。' +}; + +export const t = (text: string) => { + if (settings.get('language') !== 'zh-CN') { + return text; + } + + const systemDefaultMatch = /^System Default(\s*\(.+\))?$/.exec(text); + if (systemDefaultMatch) { + return `系统默认${systemDefaultMatch[1] ?? ''}`; + } + + return simplifiedChinese[text] ?? text; +}; diff --git a/main/common/settings.ts b/main/common/settings.ts index c6577520..02a6dfd6 100644 --- a/main/common/settings.ts +++ b/main/common/settings.ts @@ -16,6 +16,7 @@ const shortcutSchema = { }; interface Settings { + language: 'en' | 'zh-CN'; kapturesDir: string; allowAnalytics: boolean; showCursor: boolean; @@ -42,6 +43,14 @@ interface Settings { export const settings = new Store({ schema: { + language: { + type: 'string', + enum: [ + 'en', + 'zh-CN' + ], + default: 'en' + }, kapturesDir: { type: 'string', default: `${homedir()}/Movies/Kaptures` diff --git a/main/common/system-permissions.ts b/main/common/system-permissions.ts index c60f5971..c9d8f539 100644 --- a/main/common/system-permissions.ts +++ b/main/common/system-permissions.ts @@ -1,4 +1,5 @@ import {systemPreferences, shell, dialog, app} from 'electron'; +import {t} from './localization'; const {hasScreenCapturePermission, hasPromptedForPermission} = require('mac-screen-capture-permissions'); const {ensureDockIsShowing} = require('../utils/dock'); @@ -13,10 +14,10 @@ const promptSystemPreferences = (options: {message: string; detail: string; syst await ensureDockIsShowing(async () => { const {response} = await dialog.showMessageBox({ type: 'warning', - buttons: ['Open System Preferences', 'Cancel'], + buttons: [t('Open System Preferences'), t('Cancel')], defaultId: 0, - message: options.message, - detail: options.detail, + message: t(options.message), + detail: t(options.detail), cancelId: 1 }); isDialogShowing = false; @@ -86,4 +87,3 @@ export const ensureScreenCapturePermissions = (fallback = screenCaptureFallback) }; export const hasScreenCaptureAccess = () => hasScreenCapturePermission(); - diff --git a/main/menus/application.ts b/main/menus/application.ts index 27b87b21..4dd7c276 100644 --- a/main/menus/application.ts +++ b/main/menus/application.ts @@ -1,6 +1,7 @@ import {appMenu} from 'electron-util'; import {getAboutMenuItem, getExportHistoryMenuItem, getOpenFileMenuItem, getPreferencesMenuItem, getSendFeedbackMenuItem} from './common'; import {MenuItemId, MenuOptions} from './utils'; +import {t} from '../common/localization'; const getAppMenuItem = () => { const appMenuItem = appMenu([getPreferencesMenuItem()]); @@ -15,6 +16,7 @@ export const defaultApplicationMenu = (): MenuOptions => [ getAppMenuItem(), { role: 'fileMenu', + label: t('File'), id: MenuItemId.file, submenu: [ getOpenFileMenuItem(), @@ -28,10 +30,12 @@ export const defaultApplicationMenu = (): MenuOptions => [ }, { role: 'editMenu', + label: t('Edit'), id: MenuItemId.edit }, { role: 'windowMenu', + label: t('Window'), id: MenuItemId.window, submenu: [ { @@ -54,7 +58,7 @@ export const defaultApplicationMenu = (): MenuOptions => [ }, { id: MenuItemId.help, - label: 'Help', + label: t('Help'), role: 'help', submenu: [getSendFeedbackMenuItem()] } diff --git a/main/menus/cog.ts b/main/menus/cog.ts index 4c80bef1..df9633cb 100644 --- a/main/menus/cog.ts +++ b/main/menus/cog.ts @@ -6,6 +6,7 @@ import {getAudioDevices, getDefaultInputDevice} from '../utils/devices'; import {settings} from '../common/settings'; import {defaultInputDeviceId} from '../common/constants'; import {hasMicrophoneAccess} from '../common/system-permissions'; +import {t} from '../common/localization'; const getCogMenuTemplate = async (): Promise => [ getAboutMenuItem(), @@ -48,7 +49,7 @@ const getPluginsItem = (): MenuOptions[number] => { return { id: MenuItemId.plugins, - label: 'Plugins', + label: t('Plugins'), submenu: items, visible: items.length > 0 }; @@ -67,10 +68,10 @@ const getMicrophoneItem = async (): Promise => { return { id: MenuItemId.audioDevices, - label: 'Microphone', + label: t('Microphone'), submenu: [ { - label: 'None', + label: t('None'), type: 'checkbox', checked: !isRecordAudioEnabled, click: () => { @@ -78,7 +79,7 @@ const getMicrophoneItem = async (): Promise => { } }, ...[ - {name: `System Default${currentDefaultDevice ? ` (${currentDefaultDevice.name})` : ''}`, id: defaultInputDeviceId}, + {name: t(`System Default${currentDefaultDevice ? ` (${currentDefaultDevice.name})` : ''}`), id: defaultInputDeviceId}, ...devices ].map(device => ({ label: device.name, diff --git a/main/menus/common.ts b/main/menus/common.ts index 495dd126..aff6985d 100644 --- a/main/menus/common.ts +++ b/main/menus/common.ts @@ -6,17 +6,18 @@ import {supportedVideoExtensions} from '../common/constants'; import {getCurrentMenuItem, MenuItemId} from './utils'; import {openFiles} from '../utils/open-files'; import {windowManager} from '../windows/manager'; +import {t} from '../common/localization'; export const getPreferencesMenuItem = () => ({ id: MenuItemId.preferences, - label: 'Preferences…', + label: t('Preferences…'), accelerator: 'Command+,', click: () => windowManager.preferences?.open() }); export const getAboutMenuItem = () => ({ id: MenuItemId.about, - label: `About ${app.name}`, + label: t(`About ${app.name}`), click: () => { windowManager.cropper?.close(); app.focus(); @@ -26,7 +27,7 @@ export const getAboutMenuItem = () => ({ export const getOpenFileMenuItem = () => ({ id: MenuItemId.openVideo, - label: 'Open Video…', + label: t('Open Video…'), accelerator: 'Command+O', click: async () => { windowManager.cropper?.close(); @@ -35,7 +36,7 @@ export const getOpenFileMenuItem = () => ({ app.focus(); const {canceled, filePaths} = await dialog.showOpenDialog({ - filters: [{name: 'Videos', extensions: supportedVideoExtensions}], + filters: [{name: t('Videos'), extensions: supportedVideoExtensions}], properties: ['openFile', 'multiSelections'] }); @@ -46,7 +47,7 @@ export const getOpenFileMenuItem = () => ({ }); export const getExportHistoryMenuItem = () => ({ - label: 'Export History', + label: t('Export History'), click: () => windowManager.exports?.open(), enabled: getCurrentMenuItem(MenuItemId.exportHistory)?.enabled ?? false, id: MenuItemId.exportHistory @@ -54,7 +55,7 @@ export const getExportHistoryMenuItem = () => ({ export const getSendFeedbackMenuItem = () => ({ id: MenuItemId.sendFeedback, - label: 'Send Feedback…', + label: t('Send Feedback…'), click() { openNewGitHubIssue({ user: 'wulkano', @@ -91,4 +92,3 @@ Workaround: A workaround for the issue if you've found on. (this will `; - diff --git a/main/menus/record.ts b/main/menus/record.ts index 199c0258..67b9a374 100644 --- a/main/menus/record.ts +++ b/main/menus/record.ts @@ -3,6 +3,7 @@ import {MenuItemId, MenuOptions} from './utils'; import {pauseRecording, resumeRecording, stopRecording} from '../aperture'; import formatTime from '../utils/format-time'; import {getCurrentDurationStart, getOverallDuration} from '../utils/track-duration'; +import {t} from '../common/localization'; const getDurationLabel = () => { if (getCurrentDurationStart() <= 0) { @@ -20,19 +21,19 @@ const getDurationMenuItem = () => ({ const getStopRecordingMenuItem = () => ({ id: MenuItemId.stopRecording, - label: 'Stop', + label: t('Stop'), click: stopRecording }); const getPauseRecordingMenuItem = () => ({ id: MenuItemId.pauseRecording, - label: 'Pause', + label: t('Pause'), click: pauseRecording }); const getResumeRecordingMenuItem = () => ({ id: MenuItemId.resumeRecording, - label: 'Resume', + label: t('Resume'), click: resumeRecording }); diff --git a/main/windows/cropper.ts b/main/windows/cropper.ts index 895293a8..44ba5fa1 100644 --- a/main/windows/cropper.ts +++ b/main/windows/cropper.ts @@ -5,6 +5,7 @@ import delay from 'delay'; import {settings} from '../common/settings'; import {hasMicrophoneAccess, ensureMicrophonePermissions, openSystemPreferences, ensureScreenCapturePermissions} from '../common/system-permissions'; +import {t} from '../common/localization'; import {loadRoute} from '../utils/routes'; import {MacWindow} from '../utils/windows'; @@ -100,10 +101,10 @@ const openCropperWindow = async () => { const granted = await ensureMicrophonePermissions(async () => { const {response} = await dialog.showMessageBox({ type: 'warning', - buttons: ['Open System Preferences', 'Continue'], + buttons: [t('Open System Preferences'), t('Continue')], defaultId: 1, - message: 'Kap cannot access the microphone.', - detail: 'Audio recording is enabled but Kap does not have access to the microphone. Continue without audio or grant Kap access to the microphone the System Preferences.', + message: t('Kap cannot access the microphone.'), + detail: t('Audio recording is enabled but Kap does not have access to the microphone. Continue without audio or grant Kap access to the microphone the System Preferences.'), cancelId: 2 }); diff --git a/main/windows/editor.ts b/main/windows/editor.ts index 77ad4238..061c01c0 100644 --- a/main/windows/editor.ts +++ b/main/windows/editor.ts @@ -7,6 +7,7 @@ import {is} from 'electron-util'; import fs from 'fs'; import {saveSnapshot} from '../utils/image-preview'; import {windowManager} from './manager'; +import {t} from '../common/localization'; const pify = require('pify'); @@ -68,7 +69,7 @@ const open = async (video: Video) => { submenu.splice(index + 1, 0, { type: 'separator' }, { - label: 'Save Original…', + label: t('Save Original…'), id: MenuItemId.saveOriginal, accelerator: 'Command+S', click: async () => saveOriginal(video) @@ -89,13 +90,13 @@ const open = async (video: Video) => { const buttonIndex = dialog.showMessageBoxSync(editorWindow, { type: 'question', buttons: [ - 'Discard', - 'Cancel' + t('Discard'), + t('Cancel') ], defaultId: 0, cancelId: 1, - message: 'Are you sure that you want to discard this recording?', - detail: 'You will no longer be able to edit and export the original recording.' + message: t('Are you sure that you want to discard this recording?'), + detail: t('You will no longer be able to edit and export the original recording.') }); if (buttonIndex === 1) { diff --git a/main/windows/exports.ts b/main/windows/exports.ts index 2309e29e..4bc976ad 100644 --- a/main/windows/exports.ts +++ b/main/windows/exports.ts @@ -1,5 +1,6 @@ import KapWindow from './kap-window'; import {windowManager} from './manager'; +import {t} from '../common/localization'; let exportsKapWindow: KapWindow | undefined; @@ -8,7 +9,7 @@ const openExportsWindow = async () => { exportsKapWindow.browserWindow.focus(); } else { exportsKapWindow = new KapWindow({ - title: 'Exports', + title: t('Exports'), width: 320, height: 360, resizable: false, diff --git a/main/windows/preferences.ts b/main/windows/preferences.ts index b6dd366d..984083a3 100644 --- a/main/windows/preferences.ts +++ b/main/windows/preferences.ts @@ -6,6 +6,7 @@ import {ipcMain as ipc} from 'electron-better-ipc'; import {loadRoute} from '../utils/routes'; import {track} from '../common/analytics'; import {windowManager} from './manager'; +import {t} from '../common/localization'; let prefsWindow: BrowserWindow | undefined; @@ -25,7 +26,7 @@ const openPrefsWindow = async (options?: PreferencesWindowOptions) => { } prefsWindow = new BrowserWindow({ - title: 'Preferences', + title: t('Preferences'), width: 480, height: 480, resizable: false, diff --git a/renderer/components/editor/conversion/title-bar.tsx b/renderer/components/editor/conversion/title-bar.tsx index 80fbbecf..9ae6d146 100644 --- a/renderer/components/editor/conversion/title-bar.tsx +++ b/renderer/components/editor/conversion/title-bar.tsx @@ -7,6 +7,7 @@ import {ExportStatus} from '../../../common/types'; import {useMemo} from 'react'; import {template} from 'lodash'; import IconMenu from '../../icon-menu'; +import {t} from '../../../utils/localization'; const TitleBar = ({conversion, cancel, copy, retry, showInFolder}: {conversion: UseConversionState; cancel: () => any; copy: () => any; retry: () => any; showInFolder: () => void}) => { const {api} = require('electron-util'); @@ -14,8 +15,8 @@ const TitleBar = ({conversion, cancel, copy, retry, showInFolder}: {conversion: if (conversion.status === ExportStatus.inProgress && !flags.get('backgroundEditorConversion')) { await api.dialog.showMessageBox(remote.getCurrentWindow(), { type: 'info', - message: 'Your export will continue in the background. You can access it through the Export History window.', - buttons: ['Ok'], + message: t('Your export will continue in the background. You can access it through the Export History window.'), + buttons: [t('OK')], defaultId: 0 }); flags.set('backgroundEditorConversion', true); @@ -29,7 +30,7 @@ const TitleBar = ({conversion, cancel, copy, retry, showInFolder}: {conversion: if (conversion?.canCopy) { template.push({ - label: 'Copy', + label: t('Copy'), click: () => copy() }, { type: 'separator' @@ -38,7 +39,7 @@ const TitleBar = ({conversion, cancel, copy, retry, showInFolder}: {conversion: if (conversion?.status === ExportStatus.completed) { template.push({ - label: 'Show in Finder', + label: t('Show in Finder'), click: () => showInFolder() }); } diff --git a/renderer/components/editor/options/select.tsx b/renderer/components/editor/options/select.tsx index d78d7544..7aacf277 100644 --- a/renderer/components/editor/options/select.tsx +++ b/renderer/components/editor/options/select.tsx @@ -2,6 +2,7 @@ import {DropdownArrowIcon, CancelIcon} from '../../../vectors'; import classNames from 'classnames'; import {useRef} from 'react'; import {remote, MenuItemConstructorOptions, NativeImage} from 'electron'; +import {t} from '../../../utils/localization'; type Option = { label: string; @@ -57,14 +58,14 @@ const Select = (props: Props) => { if (option.subMenu) { return { - label: option.label, + label: t(option.label), submenu: option.subMenu.map(opt => convertToMenuTemplate(opt)), checked: option.checked }; } return { - label: option.label, + label: t(option.label), type: option.type as any || 'checkbox', checked: option.checked ?? (option.value === value), click: option.click ?? (() => { diff --git a/renderer/components/editor/video.tsx b/renderer/components/editor/video.tsx index 82da024e..66054dd2 100644 --- a/renderer/components/editor/video.tsx +++ b/renderer/components/editor/video.tsx @@ -4,6 +4,7 @@ import VideoMetadataContainer from './video-metadata-container'; import VideoControlsContainer from './video-controls-container'; import useEditorWindowState from 'hooks/editor/use-editor-window-state'; import {ipcRenderer as ipc} from 'electron-better-ipc'; +import {t} from '../../utils/localization'; const getVideoProps = (propsArray: Array, HTMLVideoElement>>) => { const handlers = new Map(); @@ -65,7 +66,7 @@ const Video = () => { const {Menu} = require('electron-util').api; const menu = Menu.buildFromTemplate([{ - label: 'Snapshot', + label: t('Snapshot'), click: () => { ipc.callMain('save-snapshot', video.currentTime); } diff --git a/renderer/components/exports/export.tsx b/renderer/components/exports/export.tsx index d57b030d..03535590 100644 --- a/renderer/components/exports/export.tsx +++ b/renderer/components/exports/export.tsx @@ -9,6 +9,7 @@ import useConversion from '../../hooks/editor/use-conversion'; import {ExportStatus} from '../../common/types'; import {useShowWindow} from '../../hooks/use-show-window'; import {MenuItemConstructorOptions} from 'electron/common'; +import {t} from '../../utils/localization'; const stopPropagation = event => event.stopPropagation(); @@ -42,13 +43,13 @@ const Export = ({id}: {id: string}) => { const template = useMemo(() => { const menuTemplate: MenuItemConstructorOptions[] = [{ - label: 'Open Original', + label: t('Open Original'), click: () => openInEditor() }]; if (state?.canCopy) { menuTemplate.unshift({ - label: 'Copy', + label: t('Copy'), click: () => copy() }, { type: 'separator' @@ -57,7 +58,7 @@ const Export = ({id}: {id: string}) => { if (canRetry) { menuTemplate.unshift({ - label: 'Retry', + label: t('Retry'), click: () => retry() }, { type: 'separator' diff --git a/renderer/components/preferences/categories/general.js b/renderer/components/preferences/categories/general.js index b9816767..819e0d41 100644 --- a/renderer/components/preferences/categories/general.js +++ b/renderer/components/preferences/categories/general.js @@ -55,7 +55,9 @@ class General extends React.Component { category, lossyCompression, shortcuts, - shortcutMap + shortcutMap, + language, + setLanguage } = this.props; const {showCursorSupported} = this.state; @@ -71,6 +73,20 @@ class General extends React.Component { return ( + +