feat(i18n): 后端添加国际化支持、优化前端界面

- 后端添加并实现国际化支持
- 前端引入 vue-i18n 模块(尚未添加国际化逻辑)
- 优化用户界面样式,统一输入框和标签样式
This commit is contained in:
himeditator
2025-07-03 20:36:09 +08:00
parent 3dcba07b6e
commit d608bf59c7
22 changed files with 344 additions and 229 deletions

View File

@@ -8,6 +8,8 @@
</p> </p>
</div> </div>
<p style="color:red;text-align:center;">新版本的开发正在进行中新特性包括本地字幕引擎、英日语国际化以及暗色主题还将修复已知bug和提示使用体验预计将于本月之内发布。</p>
![](./assets/media/main.png) ![](./assets/media/main.png)
## 📥 下载 ## 📥 下载
@@ -50,7 +52,7 @@ npm install
> #### 背景介绍 > #### 背景介绍
> >
> 如果你是开发者,想开发自定义字幕引擎,请查看[字幕引擎说明文档](./assets/engine-manual_zh.md)。 > 如果你是开发者,想开发自定义字幕引擎,请查看[字幕引擎说明文档](./assets/engine-manual_zh.md)。
> >
> 所谓的字幕引擎实际上是一个子程序,它会实时获取系统音频输入(录音)或输出(播放声音)的流式数据,并调用音频转文字的模型生成对应音频的字幕。生成的字幕通过 IPC 输出为转换为字符串的 JSON 数据,并返回给主程序。主程序读取字幕数据,处理后显示在窗口上。 > 所谓的字幕引擎实际上是一个子程序,它会实时获取系统音频输入(录音)或输出(播放声音)的流式数据,并调用音频转文字的模型生成对应音频的字幕。生成的字幕通过 IPC 输出为转换为字符串的 JSON 数据,并返回给主程序。主程序读取字幕数据,处理后显示在窗口上。
> >
>目前项目默认使用[阿里云 Gummy 模型](https://help.aliyun.com/zh/model-studio/gummy-speech-recognition-translation/),需要获取阿里云百炼平台的 API KEY 并配置到环境变量中才能正常使用该模型。 >目前项目默认使用[阿里云 Gummy 模型](https://help.aliyun.com/zh/model-studio/gummy-speech-recognition-translation/),需要获取阿里云百炼平台的 API KEY 并配置到环境变量中才能正常使用该模型。

View File

@@ -8,6 +8,8 @@
</p> </p>
</div> </div>
<p style="color:red;text-align:center;">The development of the new version is underway, featuring a local subtitle engine, English/Japanese internationalization, and a dark theme. It will also include fixes for known bugs and improvements to the user experience. It is expected to be released within this month.</p>
![](./assets/media/main.png) ![](./assets/media/main.png)
## ⚠️ Attention ## ⚠️ Attention
@@ -110,4 +112,4 @@ npm run build:win
npm run build:mac npm run build:mac
# For Linux # For Linux
npm run build:linux npm run build:linux
``` ```

110
package-lock.json generated
View File

@@ -1,27 +1,26 @@
{ {
"name": "auto-caption", "name": "auto-caption",
"version": "0.0.1", "version": "0.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "auto-caption", "name": "auto-caption",
"version": "0.0.1", "version": "0.1.0",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^4.0.0", "@electron-toolkit/utils": "^4.0.0",
"ant-design-vue": "^4.2.6", "ant-design-vue": "^4.2.6",
"pinia": "^3.0.2", "pinia": "^3.0.2",
"vue-router": "^4.5.1", "vue-i18n": "^11.1.9",
"ws": "^8.18.2" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@electron-toolkit/eslint-config-prettier": "3.0.0", "@electron-toolkit/eslint-config-prettier": "3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.0.0", "@electron-toolkit/eslint-config-ts": "^3.0.0",
"@electron-toolkit/tsconfig": "^1.0.1", "@electron-toolkit/tsconfig": "^1.0.1",
"@types/node": "^22.14.1", "@types/node": "^22.14.1",
"@types/ws": "^8.18.1",
"@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue": "^5.2.3",
"electron": "^35.1.5", "electron": "^35.1.5",
"electron-builder": "^25.1.8", "electron-builder": "^25.1.8",
@@ -1492,6 +1491,50 @@
"url": "https://github.com/sponsors/nzakas" "url": "https://github.com/sponsors/nzakas"
} }
}, },
"node_modules/@intlify/core-base": {
"version": "11.1.9",
"resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.1.9.tgz",
"integrity": "sha512-Lrdi4wp3XnGhWmB/mMD/XtfGUw1Jt+PGpZI/M63X1ZqhTDjNHRVCs/i8vv8U1cwaj1A9fb0bkCQHLSL0SK+pIQ==",
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "11.1.9",
"@intlify/shared": "11.1.9"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
"version": "11.1.9",
"resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.1.9.tgz",
"integrity": "sha512-84SNs3Ikjg0rD1bOuchzb3iK1vR2/8nxrkyccIl5DjFTeMzE/Fxv6X+A7RN5ZXjEWelc1p5D4kHA6HEOhlKL5Q==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "11.1.9",
"source-map-js": "^1.0.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
"version": "11.1.9",
"resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-11.1.9.tgz",
"integrity": "sha512-H/83xgU1l8ox+qG305p6ucmoy93qyjIPnvxGWRA7YdOoHe1tIiW9IlEu4lTdsOR7cfP1ecrwyflQSqXdXBacXA==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -2281,16 +2324,6 @@
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/yauzl": { "node_modules/@types/yauzl": {
"version": "2.10.3", "version": "2.10.3",
"resolved": "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz", "resolved": "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz",
@@ -9430,6 +9463,32 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/vue-i18n": {
"version": "11.1.9",
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.1.9.tgz",
"integrity": "sha512-N9ZTsXdRmX38AwS9F6Rh93RtPkvZTkSy/zNv63FTIwZCUbLwwrpqlKz9YQuzFLdlvRdZTnWAUE5jMxr8exdl7g==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.1.9",
"@intlify/shared": "11.1.9",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue-i18n/node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/vue-router": { "node_modules/vue-router": {
"version": "4.5.1", "version": "4.5.1",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz", "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz",
@@ -9581,27 +9640,6 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/ws": {
"version": "8.18.2",
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml-name-validator": { "node_modules/xml-name-validator": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz", "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz",

View File

@@ -25,6 +25,7 @@
"@electron-toolkit/utils": "^4.0.0", "@electron-toolkit/utils": "^4.0.0",
"ant-design-vue": "^4.2.6", "ant-design-vue": "^4.2.6",
"pinia": "^3.0.2", "pinia": "^3.0.2",
"vue-i18n": "^11.1.9",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {

11
src/main/i18n/index.ts Normal file
View File

@@ -0,0 +1,11 @@
import zh from './lang/zh'
import en from './lang/en'
import ja from './lang/ja'
import { allConfig } from '../utils/AllConfig'
export function i18n(key: string): string{
if(allConfig.uiLanguage === 'zh') return zh[key] || key
else if(allConfig.uiLanguage === 'en') return en[key] || key
else if(allConfig.uiLanguage === 'ja') return ja[key] || key
else return key
}

8
src/main/i18n/lang/en.ts Normal file
View File

@@ -0,0 +1,8 @@
export default {
"gummy.env.missing": "DASHSCOPE_API_KEY environment variable not detected. To use the gummy engine, you need to obtain an API Key from Alibaba Cloud's Bailian platform and add it to your local environment variables.",
"platform.unsupported": "Unsupported platform: ",
"engine.start.error": "Caption engine failed to start: ",
"engine.output.parse.error": "Unable to parse caption engine output as a JSON object: ",
"engine.error": "Caption engine error: ",
"engine.shutdown.error": "Failed to shut down the caption engine process: "
}

8
src/main/i18n/lang/ja.ts Normal file
View File

@@ -0,0 +1,8 @@
export default {
"gummy.env.missing": "DASHSCOPE_API_KEY 環境変数が検出されませんでした。Gummy エンジンを使用するには、Alibaba Cloud の百煉プラットフォームから API Key を取得し、ローカル環境変数に追加する必要があります。",
"platform.unsupported": "サポートされていないプラットフォーム: ",
"engine.start.error": "字幕エンジンの起動に失敗しました: ",
"engine.output.parse.error": "字幕エンジンの出力を JSON オブジェクトとして解析できませんでした: ",
"engine.error": "字幕エンジンエラー: ",
"engine.shutdown.error": "字幕エンジンプロセスの終了に失敗しました: "
}

8
src/main/i18n/lang/zh.ts Normal file
View File

@@ -0,0 +1,8 @@
export default {
"gummy.env.missing": "没有检测到 DASHSCOPE_API_KEY 环境变量,如果要使用 gummy 引擎,需要在阿里云百炼平台获取 API Key 并添加到本机环境变量",
"platform.unsupported": "不支持的平台:",
"engine.start.error": "字幕引擎启动失败:",
"engine.output.parse.error": "字幕引擎输出内容无法解析为 JSON 对象:",
"engine.error": "字幕引擎错误:",
"engine.shutdown.error": "字幕引擎进程关闭失败:"
}

View File

@@ -1,4 +1,4 @@
import { Styles, CaptionItem, Controls } from '../types' import { UILanguage, Styles, CaptionItem, Controls } from '../types'
import { app, BrowserWindow } from 'electron' import { app, BrowserWindow } from 'electron'
import * as path from 'path' import * as path from 'path'
import * as fs from 'fs' import * as fs from 'fs'
@@ -29,6 +29,7 @@ const defaultControls: Controls = {
class AllConfig { class AllConfig {
uiLanguage: UILanguage = 'ja'
styles: Styles = {...defaultStyles}; styles: Styles = {...defaultStyles};
controls: Controls = {...defaultControls}; controls: Controls = {...defaultControls};
captionLog: CaptionItem[] = []; captionLog: CaptionItem[] = [];
@@ -39,14 +40,16 @@ class AllConfig {
const configPath = path.join(app.getPath('userData'), 'config.json') const configPath = path.join(app.getPath('userData'), 'config.json')
if(fs.existsSync(configPath)){ if(fs.existsSync(configPath)){
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
this.setStyles(config.styles) if(config.uiLanguage) this.uiLanguage = config.uiLanguage
this.setControls(config.controls) if(config.styles) this.setStyles(config.styles)
if(config.controls) this.setControls(config.controls)
console.log('[INFO] Read Config from:', configPath) console.log('[INFO] Read Config from:', configPath)
} }
} }
public writeConfig() { public writeConfig() {
const config = { const config = {
uiLanguage: this.uiLanguage,
controls: this.controls, controls: this.controls,
styles: this.styles styles: this.styles
} }

View File

@@ -4,6 +4,7 @@ import { is } from '@electron-toolkit/utils'
import path from 'path' import path from 'path'
import { controlWindow } from '../ControlWindow' import { controlWindow } from '../ControlWindow'
import { allConfig } from './AllConfig' import { allConfig } from './AllConfig'
import { i18n } from '../i18n'
export class CaptionEngine { export class CaptionEngine {
appPath: string = '' appPath: string = ''
@@ -18,7 +19,7 @@ export class CaptionEngine {
else if (allConfig.controls.engine === 'gummy') { else if (allConfig.controls.engine === 'gummy') {
allConfig.controls.customized = false allConfig.controls.customized = false
if(!process.env.DASHSCOPE_API_KEY) { if(!process.env.DASHSCOPE_API_KEY) {
controlWindow.sendErrorMessage('没有检测到 DASHSCOPE_API_KEY 环境变量,如果要使用 gummy 引擎,需要在阿里云百炼平台获取 API Key 并添加到本机环境变量') controlWindow.sendErrorMessage(i18n('gummy.env.missing'))
return false return false
} }
let gummyName = '' let gummyName = ''
@@ -29,8 +30,8 @@ export class CaptionEngine {
gummyName = 'main-gummy' gummyName = 'main-gummy'
} }
else { else {
controlWindow.sendErrorMessage('Unsupported platform: ' + process.platform) controlWindow.sendErrorMessage(i18n('platform.unsupported') + process.platform)
throw new Error('Unsupported platform') throw new Error(i18n('platform.unsupported'))
} }
if (is.dev) { if (is.dev) {
this.appPath = path.join( this.appPath = path.join(
@@ -66,7 +67,7 @@ export class CaptionEngine {
this.process = spawn(this.appPath, this.command) this.process = spawn(this.appPath, this.command)
} }
catch (e) { catch (e) {
controlWindow.sendErrorMessage('字幕引擎启动失败:' + e) controlWindow.sendErrorMessage(i18n('engine.start.error') + e)
console.error('[ERROR] Error starting subprocess:', e) console.error('[ERROR] Error starting subprocess:', e)
return return
} }
@@ -80,7 +81,7 @@ export class CaptionEngine {
controlWindow.window.webContents.send('control.engine.started') controlWindow.window.webContents.send('control.engine.started')
} }
this.process.stdout.on('data', (data) => { this.process.stdout.on('data', (data: any) => {
const lines = data.toString().split('\n'); const lines = data.toString().split('\n');
lines.forEach((line: string) => { lines.forEach((line: string) => {
if (line.trim()) { if (line.trim()) {
@@ -88,7 +89,7 @@ export class CaptionEngine {
const caption = JSON.parse(line); const caption = JSON.parse(line);
allConfig.updateCaptionLog(caption); allConfig.updateCaptionLog(caption);
} catch (e) { } catch (e) {
controlWindow.sendErrorMessage('字幕引擎输出内容无法解析为 JSON 对象:' + e) controlWindow.sendErrorMessage(i18n('engine.output.parse.error') + e)
console.error('[ERROR] Error parsing JSON:', e); console.error('[ERROR] Error parsing JSON:', e);
} }
} }
@@ -96,7 +97,7 @@ export class CaptionEngine {
}); });
this.process.stderr.on('data', (data) => { this.process.stderr.on('data', (data) => {
controlWindow.sendErrorMessage('字幕引擎错误:' + data) controlWindow.sendErrorMessage(i18n('engine.error') + data)
console.error(`[ERROR] Subprocess Error: ${data}`); console.error(`[ERROR] Subprocess Error: ${data}`);
}); });
@@ -115,7 +116,7 @@ export class CaptionEngine {
if (process.platform === "win32" && this.process.pid) { if (process.platform === "win32" && this.process.pid) {
exec(`taskkill /pid ${this.process.pid} /t /f`, (error) => { exec(`taskkill /pid ${this.process.pid} /t /f`, (error) => {
if (error) { if (error) {
controlWindow.sendErrorMessage('字幕引擎进程关闭失败:' + error) controlWindow.sendErrorMessage(i18n('engine.shutdown.error') + error)
console.error(`[ERROR] Failed to kill process: ${error}`) console.error(`[ERROR] Failed to kill process: ${error}`)
} }
}); });

View File

@@ -0,0 +1,22 @@
.input-item {
margin: 10px 0;
}
.input-label {
display: inline-block;
width: 80px;
text-align: right;
margin-right: 10px;
}
.input-area {
width: calc(100% - 100px);
min-width: 100px;
}
.input-item-value {
width: 80px;
text-align: right;
font-size: 12px;
color: #666
}

View File

@@ -5,57 +5,57 @@
<a @click="backStyle">取消更改</a> | <a @click="backStyle">取消更改</a> |
<a @click="resetStyle">恢复默认</a> <a @click="resetStyle">恢复默认</a>
</template> </template>
<div class="style-item"> <div class="input-item">
<span class="style-label">字体族</span> <span class="input-label">字体族</span>
<a-input <a-input
class="style-input" class="input-area"
v-model:value="currentFontFamily" v-model:value="currentFontFamily"
/> />
</div> </div>
<div class="style-item"> <div class="input-item">
<span class="style-label">字体颜色</span> <span class="input-label">字体颜色</span>
<a-input <a-input
class="style-input" class="input-area"
type="color" type="color"
v-model:value="currentFontColor" v-model:value="currentFontColor"
/> />
<div class="style-item-value">{{ currentFontColor }}</div> <div class="input-item-value">{{ currentFontColor }}</div>
</div> </div>
<div class="style-item"> <div class="input-item">
<span class="style-label">字体大小</span> <span class="input-label">字体大小</span>
<a-input <a-input
class="style-input" class="input-area"
type="range" type="range"
min="0" max="64" min="0" max="64"
v-model:value="currentFontSize" v-model:value="currentFontSize"
/> />
<div class="style-item-value">{{ currentFontSize }}px</div> <div class="input-item-value">{{ currentFontSize }}px</div>
</div> </div>
<div class="style-item"> <div class="input-item">
<span class="style-label">背景颜色</span> <span class="input-label">背景颜色</span>
<a-input <a-input
class="style-input" class="input-area"
type="color" type="color"
v-model:value="currentBackground" v-model:value="currentBackground"
/> />
<div class="style-item-value">{{ currentBackground }}</div> <div class="input-item-value">{{ currentBackground }}</div>
</div> </div>
<div class="style-item"> <div class="input-item">
<span class="style-label">背景透明度</span> <span class="input-label">背景透明度</span>
<a-input <a-input
class="style-input" class="input-area"
type="range" type="range"
min="0" min="0"
max="100" max="100"
v-model:value="currentOpacity" v-model:value="currentOpacity"
/> />
<div class="style-item-value">{{ currentOpacity }}</div> <div class="input-item-value">{{ currentOpacity }}</div>
</div> </div>
<div class="style-item"> <div class="input-item">
<span class="style-label">显示预览</span> <span class="input-label">显示预览</span>
<a-switch v-model:checked="displayPreview" /> <a-switch v-model:checked="displayPreview" />
<span class="style-label">显示翻译</span> <span class="input-label">显示翻译</span>
<a-switch v-model:checked="currentTransDisplay" /> <a-switch v-model:checked="currentTransDisplay" />
</div> </div>
@@ -64,31 +64,31 @@
<template #extra> <template #extra>
<a @click="useSameStyle">使用相同样式</a> <a @click="useSameStyle">使用相同样式</a>
</template> </template>
<div class="style-item"> <div class="input-item">
<span class="style-label">翻译字体</span> <span class="input-label">翻译字体</span>
<a-input <a-input
class="style-input" class="input-area"
v-model:value="currentTransFontFamily" v-model:value="currentTransFontFamily"
/> />
</div> </div>
<div class="style-item"> <div class="input-item">
<span class="style-label">翻译颜色</span> <span class="input-label">翻译颜色</span>
<a-input <a-input
class="style-input" class="input-area"
type="color" type="color"
v-model:value="currentTransFontColor" v-model:value="currentTransFontColor"
/> />
<div class="style-item-value">{{ currentTransFontColor }}</div> <div class="input-item-value">{{ currentTransFontColor }}</div>
</div> </div>
<div class="style-item"> <div class="input-item">
<span class="style-label">翻译大小</span> <span class="input-label">翻译大小</span>
<a-input <a-input
class="style-input" class="input-area"
type="range" type="range"
min="0" max="64" min="0" max="64"
v-model:value="currentTransFontSize" v-model:value="currentTransFontSize"
/> />
<div class="style-item-value">{{ currentTransFontSize }}px</div> <div class="input-item-value">{{ currentTransFontSize }}px</div>
</div> </div>
</a-card> </a-card>
</div> </div>
@@ -115,10 +115,10 @@
:style="{ :style="{
fontFamily: currentTransFontFamily, fontFamily: currentTransFontFamily,
fontSize: currentTransFontSize + 'px', fontSize: currentTransFontSize + 'px',
color: currentTransFontColor color: currentTransFontColor
}" }"
>这是字幕样式预览(翻译)</p> >这是字幕样式预览(翻译)</p>
</div> </div>
</Teleport> </Teleport>
</template> </template>
@@ -154,7 +154,7 @@ function useSameStyle(){
currentTransFontColor.value = currentFontColor.value; currentTransFontColor.value = currentFontColor.value;
} }
function applyStyle(){ function applyStyle(){
captionStyle.fontFamily = currentFontFamily.value; captionStyle.fontFamily = currentFontFamily.value;
captionStyle.fontSize = currentFontSize.value; captionStyle.fontSize = currentFontSize.value;
captionStyle.fontColor = currentFontColor.value; captionStyle.fontColor = currentFontColor.value;
@@ -182,7 +182,7 @@ function backStyle(){
currentTransFontColor.value = captionStyle.transFontColor; currentTransFontColor.value = captionStyle.transFontColor;
} }
function resetStyle() { function resetStyle() {
captionStyle.sendStyleReset(); captionStyle.sendStyleReset();
} }
@@ -195,33 +195,7 @@ watch(changeSignal, (val) => {
</script> </script>
<style scoped> <style scoped>
.caption-button { @import url(../assets/input.css);
display: flex;
justify-content: center;
}
.style-item {
margin: 10px 0;
}
.style-label {
display: inline-block;
width: 80px;
text-align: right;
margin-right: 10px;
}
.style-input {
width: calc(100% - 100px);
min-width: 100px;
}
.style-item-value {
width: 80px;
text-align: right;
font-size: 12px;
color: #666
}
.preview-container { .preview-container {
line-height: 2em; line-height: 2em;
@@ -239,4 +213,4 @@ watch(changeSignal, (val) => {
margin: 0; margin: 0;
line-height: 1.5em; line-height: 1.5em;
} }
</style> </style>

View File

@@ -5,58 +5,58 @@
<a @click="applyChange">更改设置</a> | <a @click="applyChange">更改设置</a> |
<a @click="cancelChange">取消更改</a> <a @click="cancelChange">取消更改</a>
</template> </template>
<div class="control-item"> <div class="input-item">
<span class="control-label">源语言</span> <span class="input-label">源语言</span>
<a-select <a-select
class="control-input" class="input-area"
v-model:value="currentSourceLang" v-model:value="currentSourceLang"
:options="langList" :options="langList"
></a-select> ></a-select>
</div> </div>
<div class="control-item"> <div class="input-item">
<span class="control-label">翻译语言</span> <span class="input-label">翻译语言</span>
<a-select <a-select
class="control-input" class="input-area"
v-model:value="currentTargetLang" v-model:value="currentTargetLang"
:options="langList.filter((item) => item.value !== 'auto')" :options="langList.filter((item) => item.value !== 'auto')"
></a-select> ></a-select>
</div> </div>
<div class="control-item"> <div class="input-item">
<span class="control-label">字幕引擎</span> <span class="input-label">字幕引擎</span>
<a-select <a-select
class="control-input" class="input-area"
v-model:value="currentEngine" v-model:value="currentEngine"
:options="captionEngine" :options="captionEngine"
></a-select> ></a-select>
</div> </div>
<div class="control-item"> <div class="input-item">
<span class="control-label">音频选择</span> <span class="input-label">音频选择</span>
<a-select <a-select
class="control-input" class="input-area"
v-model:value="currentAudio" v-model:value="currentAudio"
:options="audioType" :options="audioType"
></a-select> ></a-select>
</div> </div>
<div class="control-item"> <div class="input-item">
<span class="control-label">启用翻译</span> <span class="input-label">启用翻译</span>
<a-switch v-model:checked="currentTranslation" /> <a-switch v-model:checked="currentTranslation" />
<span class="control-label">自定义引擎</span> <span class="input-label">自定义引擎</span>
<a-switch v-model:checked="currentCustomized" /> <a-switch v-model:checked="currentCustomized" />
</div> </div>
<div v-show="currentCustomized"> <div v-show="currentCustomized">
<a-card size="small" title="自定义字幕引擎"> <a-card size="small" title="自定义字幕引擎">
<p class="customize-note">说明允许用户使用自定义字幕引擎提供字幕提供的引擎要能通过 <code>child_process.spawn()</code> 进行启动且需要通过 IPC 与项目 node.js 后端进行通信具体通信接口见后端实现</p> <p class="customize-note">说明允许用户使用自定义字幕引擎提供字幕提供的引擎要能通过 <code>child_process.spawn()</code> 进行启动且需要通过 IPC 与项目 node.js 后端进行通信具体通信接口见后端实现</p>
<div class="control-item"> <div class="input-item">
<span class="control-label">引擎路径</span> <span class="input-label">引擎路径</span>
<a-input <a-input
class="control-input" class="input-area"
v-model:value="currentCustomizedApp" v-model:value="currentCustomizedApp"
></a-input> ></a-input>
</div> </div>
<div class="control-item"> <div class="input-item">
<span class="control-label">引擎指令</span> <span class="input-label">引擎指令</span>
<a-input <a-input
class="control-input" class="input-area"
v-model:value="currentCustomizedCommand" v-model:value="currentCustomizedCommand"
></a-input> ></a-input>
</div> </div>
@@ -134,32 +134,11 @@ watch(changeSignal, (val) => {
</script> </script>
<style scoped> <style scoped>
.control-item { @import url(../assets/input.css);
margin: 10px 0;
}
.control-label {
display: inline-block;
width: 80px;
text-align: right;
margin-right: 10px;
}
.customize-note { .customize-note {
padding: 0 20px; padding: 0 20px;
color: red; color: red;
font-size: 12px; font-size: 12px;
} }
.control-input {
width: calc(100% - 100px);
min-width: 100px;
}
.control-item-value {
width: 80px;
text-align: right;
font-size: 12px;
color: #666
}
</style> </style>

View File

@@ -10,6 +10,10 @@
<a-col :span="6"> <a-col :span="6">
<a-statistic title="已记录字幕" :value="captionData.length" /> <a-statistic title="已记录字幕" :value="captionData.length" />
</a-col> </a-col>
<a-col :span="6">
<div class="about-tag">关于本项目</div>
<GithubOutlined class="proj-info" @click="showAbout = true"/>
</a-col>
</a-row> </a-row>
</div> </div>
@@ -28,12 +32,45 @@
@click="stopEngine" @click="stopEngine"
>关闭字幕引擎</a-button> >关闭字幕引擎</a-button>
</div> </div>
<a-modal v-model:open="showAbout" title="关于本项目" :footer="null">
<div class="about-modal-content">
<h2 class="about-title">Auto Caption 项目</h2>
<p class="about-desc">一个跨平台的实时字幕显示软件</p>
<a-divider />
<div class="about-info">
<p><b>作者</b>HiMeditator</p>
<p><b>版本</b>v0.1.0</p>
<p>
<b>项目地址</b>
<a href="https://github.com/HiMeditator/auto-caption" target="_blank">
GitHub | auto-caption
</a>
</p>
<p>
<b>用户手册</b>
<a
href="https://github.com/HiMeditator/auto-caption/blob/main/assets/user-manual_zh.md"
target="_blank"
>
GitHub | user-manual_zh.md
</a>
</p>
</div>
<div class="about-date">2026 6 26 </div>
</div>
</a-modal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useCaptionLogStore } from '@renderer/stores/captionLog' import { useCaptionLogStore } from '@renderer/stores/captionLog'
import { useEngineControlStore } from '@renderer/stores/engineControl' import { useEngineControlStore } from '@renderer/stores/engineControl'
import { GithubOutlined } from '@ant-design/icons-vue';
const showAbout = ref(false)
const captionLog = useCaptionLogStore() const captionLog = useCaptionLogStore()
const { captionData } = storeToRefs(captionLog) const { captionData } = storeToRefs(captionLog)
const engineControl = useEngineControlStore() const engineControl = useEngineControlStore()
@@ -53,6 +90,48 @@ function stopEngine() {
</script> </script>
<style scoped> <style scoped>
.about-tag {
color: rgba(0,0,0,0.45);
margin-bottom: 16px;
}
.proj-info {
display: inline-block;
font-size: 24px;
cursor: pointer;
color: #1f2328;
}
.about-modal-content {
text-align: center;
padding: 8px 0 0 0;
}
.about-title {
font-size: 1.5em;
font-weight: bold;
margin-bottom: 0.2em;
}
.about-desc {
color: #666;
margin-bottom: 0.5em;
}
.about-info {
text-align: left;
display: inline-block;
margin: 0 auto;
font-size: 1em;
}
.about-date {
margin-top: 1.5em;
color: #aaa;
font-size: 0.95em;
text-align: right;
}
.caption-control { .caption-control {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

@@ -1,53 +1,29 @@
<template> <template>
<a-card size="small" title="页面宽度"> <a-card size="small" title="通用设置">
<template #extra>
<a-button type="link" @click="showAbout = true">关于本项目</a-button>
</template>
<div> <div>
<a-input type="range" class="span-input" min="6" max="18" v-model:value="leftBarWidth" /> <div class="input-item">
<span class="input-label">边栏宽度</span>
<a-input
type="range" class="span-input"
min="6" max="12" v-model:value="leftBarWidth"
/>
<div class="input-item-value">{{ (leftBarWidth * 100 / 24).toFixed(0) }}%</div>
</div>
</div> </div>
</a-card> </a-card>
<a-modal v-model:open="showAbout" title="关于本项目" :footer="null">
<div class="about-modal-content">
<h2 class="about-title">Auto Caption 项目</h2>
<p class="about-desc">一个跨平台的实时字幕显示软件</p>
<a-divider />
<div class="about-info">
<p><b>作者</b>HiMeditator</p>
<p><b>版本</b>v0.1.0</p>
<p>
<b>项目地址</b>
<a href="https://github.com/HiMeditator/auto-caption" target="_blank">
GitHub | auto-caption
</a>
</p>
<p>
<b>用户手册</b>
<a
href="https://github.com/HiMeditator/auto-caption/blob/main/assets/user-manual_zh.md"
target="_blank"
>
GitHub | user-manual_zh.md
</a>
</p>
</div>
<div class="about-date">2026 6 26 </div>
</div>
</a-modal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useGeneralSettingStore } from '@renderer/stores/generalSetting' import { useGeneralSettingStore } from '@renderer/stores/generalSetting'
const generalSettingStore = useGeneralSettingStore() const generalSettingStore = useGeneralSettingStore()
const { leftBarWidth } = storeToRefs(generalSettingStore) const { leftBarWidth } = storeToRefs(generalSettingStore)
const showAbout = ref(false)
</script> </script>
<style scoped> <style scoped>
@import url(../assets/input.css);
.span-input { .span-input {
width: 100px; width: 100px;
} }

View File

@@ -0,0 +1,17 @@
import { createI18n } from 'vue-i18n';
import zh from './lang/zh';
import en from './lang/en';
import ja from './lang/ja';
const i18n = createI18n({
legacy: false,
locale: 'zh',
messages: {
zh,
en,
ja
}
});
export default i18n;

View File

@@ -0,0 +1,3 @@
export default {
}

View File

@@ -0,0 +1,3 @@
export default {
}

View File

@@ -0,0 +1,8 @@
export default {
example: {
"original": "This is a preview of subtitle styles.",
"translation": "这是字幕样式预览(翻译)"
},
general: {
}
}

View File

@@ -4,12 +4,14 @@ import { createPinia } from 'pinia'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import i18n from './i18n'
import Antd from 'ant-design-vue'; import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/reset.css'; import 'ant-design-vue/dist/reset.css';
const app = createApp(App) const app = createApp(App)
app.use(createPinia()) app.use(createPinia())
app.use(router) app.use(router)
app.use(i18n)
app.use(Antd) app.use(Antd)
app.mount('#app') app.mount('#app')

View File

@@ -26,7 +26,7 @@
color: captionStyle.fontColor color: captionStyle.fontColor
}"> }">
<span v-if="captionData.length">{{ captionData[captionData.length-1].text }}</span> <span v-if="captionData.length">{{ captionData[captionData.length-1].text }}</span>
<span v-else>{{ "This is a preview of subtitle styles." }}</span> <span v-else>{{ $t('example.original') }}</span>
</p> </p>
<p class="preview-translation" v-if="captionStyle.transDisplay" :style="{ <p class="preview-translation" v-if="captionStyle.transDisplay" :style="{
fontFamily: captionStyle.transFontFamily, fontFamily: captionStyle.transFontFamily,
@@ -34,7 +34,7 @@
color: captionStyle.transFontColor color: captionStyle.transFontColor
}"> }">
<span v-if="captionData.length">{{ captionData[captionData.length-1].translation }}</span> <span v-if="captionData.length">{{ captionData[captionData.length-1].translation }}</span>
<span v-else>{{ "这是字幕样式预览(翻译)" }}</span> <span v-else>{{ $t('example.translation') }}</span>
</p> </p>
</div> </div>
</div> </div>

View File

@@ -44,34 +44,4 @@ const { leftBarWidth } = storeToRefs(generalSettingStore)
overflow-y: auto; overflow-y: auto;
scrollbar-width: thin; scrollbar-width: thin;
} }
.about-modal-content {
text-align: center;
padding: 8px 0 0 0;
}
.about-title {
font-size: 1.5em;
font-weight: bold;
margin-bottom: 0.2em;
}
.about-desc {
color: #666;
margin-bottom: 0.5em;
}
.about-info {
text-align: left;
display: inline-block;
margin: 0 auto;
font-size: 1em;
}
.about-date {
margin-top: 1.5em;
color: #aaa;
font-size: 0.95em;
text-align: right;
}
</style> </style>