From 13a45a817bd67a0f2ffaa54ae88c2046c69bff42 Mon Sep 17 00:00:00 2001 From: Sebastian Boehler Date: Thu, 21 Aug 2025 12:01:28 +0200 Subject: [PATCH] feat: english support --- electron/main.ts | 95 +++++++++- package.json | 3 +- pnpm-lock.yaml | 40 ++++ src/i18n.ts | 200 ++++++++++++++++++++ src/layout/default.vue | 5 +- src/main.ts | 16 +- src/views/Home/components/text-generate.vue | 42 ++-- src/views/Home/components/tts-control.vue | 51 +++-- src/views/Home/components/video-manage.vue | 24 +-- src/views/Home/components/video-render.vue | 52 ++--- src/views/Home/index.vue | 24 +-- 11 files changed, 459 insertions(+), 93 deletions(-) create mode 100644 src/i18n.ts diff --git a/electron/main.ts b/electron/main.ts index 2922c6d..cf9dce1 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,4 +1,5 @@ -import { app, BrowserWindow, screen } from 'electron' +import { app, BrowserWindow, screen, Menu } from 'electron' +import type { MenuItemConstructorOptions } from 'electron' import { fileURLToPath } from 'node:url' import path from 'node:path' import GlobalSetting from '../setting.global' @@ -69,6 +70,95 @@ function createWindow() { } } +function buildMenu() { + const template: MenuItemConstructorOptions[] = [ + // macOS standard app menu + ...(process.platform === 'darwin' + ? [ + { + label: app.name, + submenu: [ + { role: 'about' }, + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideOthers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' }, + ] as MenuItemConstructorOptions[], + }, + ] + : []), + { + label: 'Language', + submenu: [ + { + label: 'English', + type: 'radio', + checked: true, + click: () => { + BrowserWindow.getAllWindows().forEach((w) => w.webContents.send('set-locale', 'en')) + }, + }, + { + label: '中文', + type: 'radio', + click: () => { + BrowserWindow.getAllWindows().forEach((w) => w.webContents.send('set-locale', 'zh-CN')) + }, + }, + ] as MenuItemConstructorOptions[], + }, + { + label: 'Edit', + submenu: [ + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + { role: 'selectAll' }, + ] as MenuItemConstructorOptions[], + }, + { + label: 'View', + submenu: [ + { role: 'reload' }, + { role: 'forceReload' }, + { role: 'toggleDevTools' }, + { type: 'separator' }, + { role: 'resetZoom' }, + { role: 'zoomIn' }, + { role: 'zoomOut' }, + { type: 'separator' }, + { role: 'togglefullscreen' }, + ] as MenuItemConstructorOptions[], + }, + { + role: 'window', + submenu: [{ role: 'minimize' }, { role: 'close' }] as MenuItemConstructorOptions[], + }, + { + role: 'help', + submenu: [ + { + label: 'Learn More', + click: async () => { + const { shell } = await import('electron') + await shell.openExternal('https://github.com/YILS-LIN/short-video-factory') + }, + }, + ] as MenuItemConstructorOptions[], + }, + ] + + const menu = Menu.buildFromTemplate(template) + Menu.setApplicationMenu(menu) +} + //关闭所有窗口后退出,macOS除外。在那里,这很常见 //让应用程序及其菜单栏保持活动状态,直到用户退出 //显式使用Cmd+Q。 @@ -101,4 +191,7 @@ app.whenReady().then(() => { app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors') // 允许本地网络请求 app.commandLine.appendSwitch('disable-features', 'BlockInsecurePrivateNetworkRequests') + + // Build application menu + buildMenu() }) diff --git a/package.json b/package.json index 1a4b272..aa7f3f6 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "ffmpeg-static": "^5.2.0", "music-metadata": "^11.7.3", "subtitle": "4.2.2-alpha.0", - "ws": "^8.18.3" + "ws": "^8.18.3", + "vue-i18n": "^9.14.0" }, "devDependencies": { "@ai-sdk/openai": "^1.3.23", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4fb6ac..47d0adc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: subtitle: specifier: 4.2.2-alpha.0 version: 4.2.2-alpha.0 + vue-i18n: + specifier: ^9.14.0 + version: 9.14.5(vue@3.5.17(typescript@5.6.2)) ws: specifier: ^8.18.3 version: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) @@ -497,6 +500,18 @@ packages: '@iconify/utils@2.3.0': resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==} + '@intlify/core-base@9.14.5': + resolution: {integrity: sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==} + engines: {node: '>= 16'} + + '@intlify/message-compiler@9.14.5': + resolution: {integrity: sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==} + engines: {node: '>= 16'} + + '@intlify/shared@9.14.5': + resolution: {integrity: sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==} + engines: {node: '>= 16'} + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -2754,6 +2769,12 @@ packages: peerDependencies: vue: ^3.4.37 + vue-i18n@9.14.5: + resolution: {integrity: sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==} + engines: {node: '>= 16'} + peerDependencies: + vue: ^3.0.0 + vue-router@4.5.1: resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==} peerDependencies: @@ -3262,6 +3283,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@intlify/core-base@9.14.5': + dependencies: + '@intlify/message-compiler': 9.14.5 + '@intlify/shared': 9.14.5 + + '@intlify/message-compiler@9.14.5': + dependencies: + '@intlify/shared': 9.14.5 + source-map-js: 1.2.1 + + '@intlify/shared@9.14.5': {} + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -5712,6 +5745,13 @@ snapshots: dependencies: vue: 3.5.17(typescript@5.6.2) + vue-i18n@9.14.5(vue@3.5.17(typescript@5.6.2)): + dependencies: + '@intlify/core-base': 9.14.5 + '@intlify/shared': 9.14.5 + '@vue/devtools-api': 6.6.4 + vue: 3.5.17(typescript@5.6.2) + vue-router@4.5.1(vue@3.5.17(typescript@5.6.2)): dependencies: '@vue/devtools-api': 6.6.4 diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..2ecd6ec --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,200 @@ +import { createI18n } from 'vue-i18n' + +const messages = { + en: { + app: { + name: 'AI Short Video Factory', + }, + prompt: { label: 'Prompt' }, + actions: { generate: 'Generate', stop: 'Stop', config: 'Configure', refreshAssets: 'Refresh Assets' }, + llm: { + configTitle: 'Configure LLM API', + modelName: 'Model Name', + apiUrl: 'API URL', + apiKey: 'API Key', + compatibleNote: 'Compatible with any OpenAI-compatible API', + connectSuccess: 'LLM connected successfully', + connectFailedPrefix: 'LLM connection failed, please check your configuration', + }, + common: { close: 'Close', test: 'Test', save: 'Save', select: 'Select', noData: 'No data' }, + output: { label: 'Output Text (editable)' }, + errors: { + promptRequired: 'Prompt cannot be empty', + generateFailedPrefix: 'Generation failed, please check LLM configuration', + outputFileNameRequired: 'Please set an output file name first', + outputPathRequired: 'Please set an export folder first', + outputSizeRequired: 'Please set output resolution (width x height) first', + bgmListFailed: 'Failed to load BGM list. Please check the folder exists', + ttsFailedCorrupt: 'TTS failed: audio file is corrupted', + ttsZeroDuration: 'TTS duration is 0s. Check TTS config and network connectivity', + renderFailedPrefix: 'Video rendering failed. Please check all configurations', + assetsDurationInsufficient: 'Total assets duration is insufficient', + edgeTtsListFailed: 'Failed to fetch Edge TTS voice list. Please check your network', + ttsConfigInvalid: 'TTS configuration is invalid', + ttsSynthesisFailed: 'TTS synthesis failed', + }, + success: { renderSuccess: 'Video rendered successfully' }, + info: { batchNext: 'Start next render', renderCanceled: 'Video rendering canceled' }, + empty: { + noContent: 'No content yet', + hintSelectFolder: 'Please choose a folder above with enough storyboard assets', + }, + videoManage: { + assetsFolderLabel: 'Storyboard assets folder', + noMp4InFolder: 'No MP4 video files found in the selected folder', + emptyFolder: 'The selected folder is empty', + readSuccess: 'Assets loaded successfully', + readFailed: 'Failed to read assets. Please check if the folder exists', + }, + dialogs: { + selectAssetsFolderTitle: 'Select storyboard assets folder', + selectOutputFolderTitle: 'Select video export folder', + selectBgmFolderTitle: 'Select background music folder', + renderConfigTitle: 'Configure render options', + }, + render: { + status: { + idle: 'Idle, ready to render', + generatingText: 'Generating script with AI LLM', + synthesizingSpeech: 'Synthesizing speech with TTS', + segmentingVideo: 'Processing video segments', + rendering: 'Rendering video', + success: 'Rendered successfully, you can start the next one', + failed: 'Render failed, please try again', + }, + startRender: 'Start Rendering', + stopRender: 'Stop Rendering', + autoBatch: 'Auto batch render', + bgmFolderLabel: 'Background music folder (.mp3, pick randomly)', + output: { + width: 'Output width', + height: 'Output height', + fileName: 'Output file name', + format: 'Output format', + folder: 'Output folder', + }, + }, + tts: { + language: 'Language', + gender: 'Gender', + voice: 'Voice', + speed: 'Speed', + tryText: 'Try-listen text', + tryListen: 'Try listen', + selectLanguageGenderFirst: 'Please select language and gender first', + selectVoiceWarning: 'Please select a voice', + tryTextEmptyWarning: 'Try-listen text cannot be empty', + playTryAudio: 'Playing try-listen audio', + trySynthesisFailedNetwork: 'Failed to synthesize try-listen audio. Please check network', + genderMale: 'Male', + genderFemale: 'Female', + speedSlow: 'Slow', + speedMedium: 'Medium', + speedFast: 'Fast', + }, + footer: { poweredBy: 'Powered by YILS (Blog: https://yils.blog)' }, + }, + 'zh-CN': { + app: { name: 'AI Short Video Factory - 短视频工厂' }, + prompt: { label: '提示词' }, + actions: { generate: '生成', stop: '停止', config: '配置', refreshAssets: '刷新素材库' }, + llm: { + configTitle: '配置大语言模型接口', + modelName: '模型名称', + apiUrl: 'API 地址', + apiKey: 'API Key', + compatibleNote: '兼容任意 OpenAI 标准接口', + connectSuccess: '大模型连接成功', + connectFailedPrefix: '大模型连接失败,请检查配置是否正确', + }, + common: { close: '关闭', test: '测试', save: '保存', select: '选择', noData: '无数据' }, + output: { label: '输出文案(可编辑)' }, + errors: { + promptRequired: '提示词不能为空', + generateFailedPrefix: '生成失败,请检查大模型配置是否正确', + outputFileNameRequired: '请先配置导出文件名', + outputPathRequired: '请先配置导出文件夹', + outputSizeRequired: '请先配置导出分辨率(宽高)', + bgmListFailed: '获取背景音乐列表失败,请检查文件夹是否存在', + ttsFailedCorrupt: '语音合成失败,音频文件损坏', + ttsZeroDuration: '语音时长为0秒,检查TTS语音合成配置及网络连接是否正常', + renderFailedPrefix: '视频合成失败,请检查各项配置是否正确', + assetsDurationInsufficient: '素材总时长不足', + edgeTtsListFailed: '获取EdgeTTS语音列表失败,请检查网络', + ttsConfigInvalid: 'TTS语音合成配置无效', + ttsSynthesisFailed: '语音合成失败', + }, + success: { renderSuccess: '视频合成成功' }, + info: { batchNext: '开始合成下一个', renderCanceled: '视频合成已终止' }, + empty: { + noContent: '暂无内容', + hintSelectFolder: '从上面选择一个包含足够分镜素材的文件夹', + }, + videoManage: { + assetsFolderLabel: '分镜视频素材文件夹', + noMp4InFolder: '选择的文件夹中不包含MP4视频文件', + emptyFolder: '选择的文件夹为空', + readSuccess: '素材读取成功', + readFailed: '素材读取失败,请检查文件夹是否存在', + }, + dialogs: { + selectAssetsFolderTitle: '选择分镜素材文件夹', + selectOutputFolderTitle: '选择视频导出文件夹', + selectBgmFolderTitle: '选择背景音乐文件夹', + renderConfigTitle: '配置合成选项', + }, + render: { + status: { + idle: '空闲,可以开始合成', + generatingText: '正在使用 AI 大模型生成文案', + synthesizingSpeech: '正在使用 TTS 合成语音', + segmentingVideo: '正在处理分镜素材', + rendering: '正在渲染视频', + success: '渲染成功,可以开始下一个', + failed: '渲染失败,请重新尝试', + }, + startRender: '开始合成', + stopRender: '停止合成', + autoBatch: '自动批量合成', + bgmFolderLabel: '背景音乐文件夹(.mp3格式,从中随机选取)', + output: { + width: '导出视频宽度', + height: '导出视频高度', + fileName: '导出文件名', + format: '导出格式', + folder: '导出文件夹', + }, + }, + tts: { + language: '语言', + gender: '性别', + voice: '声音', + speed: '语速', + tryText: '试听文本', + tryListen: '试听', + selectLanguageGenderFirst: '请先选择语言和性别', + selectVoiceWarning: '请选择一个声音', + tryTextEmptyWarning: '试听文本不能为空', + playTryAudio: '播放试听语音', + trySynthesisFailedNetwork: '试听语音合成失败,请检查网络', + genderMale: '男性', + genderFemale: '女性', + speedSlow: '慢', + speedMedium: '中', + speedFast: '快', + }, + footer: { poweredBy: 'Powered by YILS(博客地址:https://yils.blog)' }, + }, +} + +const stored = (typeof localStorage !== 'undefined' && localStorage.getItem('locale')) || undefined +const locale = stored || 'en' + +const i18n = createI18n({ + legacy: false, + locale, + fallbackLocale: 'en', + messages, +}) + +export default i18n diff --git a/src/layout/default.vue b/src/layout/default.vue index a25438b..ffe5586 100644 --- a/src/layout/default.vue +++ b/src/layout/default.vue @@ -2,7 +2,7 @@
@@ -21,12 +21,13 @@