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

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

View File

@@ -4,6 +4,7 @@ import { is } from '@electron-toolkit/utils'
import path from 'path'
import { controlWindow } from '../ControlWindow'
import { allConfig } from './AllConfig'
import { i18n } from '../i18n'
export class CaptionEngine {
appPath: string = ''
@@ -18,7 +19,7 @@ export class CaptionEngine {
else if (allConfig.controls.engine === 'gummy') {
allConfig.controls.customized = false
if(!process.env.DASHSCOPE_API_KEY) {
controlWindow.sendErrorMessage('没有检测到 DASHSCOPE_API_KEY 环境变量,如果要使用 gummy 引擎,需要在阿里云百炼平台获取 API Key 并添加到本机环境变量')
controlWindow.sendErrorMessage(i18n('gummy.env.missing'))
return false
}
let gummyName = ''
@@ -29,8 +30,8 @@ export class CaptionEngine {
gummyName = 'main-gummy'
}
else {
controlWindow.sendErrorMessage('Unsupported platform: ' + process.platform)
throw new Error('Unsupported platform')
controlWindow.sendErrorMessage(i18n('platform.unsupported') + process.platform)
throw new Error(i18n('platform.unsupported'))
}
if (is.dev) {
this.appPath = path.join(
@@ -66,7 +67,7 @@ export class CaptionEngine {
this.process = spawn(this.appPath, this.command)
}
catch (e) {
controlWindow.sendErrorMessage('字幕引擎启动失败:' + e)
controlWindow.sendErrorMessage(i18n('engine.start.error') + e)
console.error('[ERROR] Error starting subprocess:', e)
return
}
@@ -80,7 +81,7 @@ export class CaptionEngine {
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');
lines.forEach((line: string) => {
if (line.trim()) {
@@ -88,7 +89,7 @@ export class CaptionEngine {
const caption = JSON.parse(line);
allConfig.updateCaptionLog(caption);
} catch (e) {
controlWindow.sendErrorMessage('字幕引擎输出内容无法解析为 JSON 对象:' + e)
controlWindow.sendErrorMessage(i18n('engine.output.parse.error') + e)
console.error('[ERROR] Error parsing JSON:', e);
}
}
@@ -96,7 +97,7 @@ export class CaptionEngine {
});
this.process.stderr.on('data', (data) => {
controlWindow.sendErrorMessage('字幕引擎错误:' + data)
controlWindow.sendErrorMessage(i18n('engine.error') + data)
console.error(`[ERROR] Subprocess Error: ${data}`);
});
@@ -115,7 +116,7 @@ export class CaptionEngine {
if (process.platform === "win32" && this.process.pid) {
exec(`taskkill /pid ${this.process.pid} /t /f`, (error) => {
if (error) {
controlWindow.sendErrorMessage('字幕引擎进程关闭失败:' + error)
controlWindow.sendErrorMessage(i18n('engine.shutdown.error') + 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="resetStyle">恢复默认</a>
</template>
<div class="style-item">
<span class="style-label">字体族</span>
<div class="input-item">
<span class="input-label">字体族</span>
<a-input
class="style-input"
class="input-area"
v-model:value="currentFontFamily"
/>
/>
</div>
<div class="style-item">
<span class="style-label">字体颜色</span>
<div class="input-item">
<span class="input-label">字体颜色</span>
<a-input
class="style-input"
class="input-area"
type="color"
v-model:value="currentFontColor"
/>
<div class="style-item-value">{{ currentFontColor }}</div>
<div class="input-item-value">{{ currentFontColor }}</div>
</div>
<div class="style-item">
<span class="style-label">字体大小</span>
<div class="input-item">
<span class="input-label">字体大小</span>
<a-input
class="style-input"
class="input-area"
type="range"
min="0" max="64"
v-model:value="currentFontSize"
/>
<div class="style-item-value">{{ currentFontSize }}px</div>
/>
<div class="input-item-value">{{ currentFontSize }}px</div>
</div>
<div class="style-item">
<span class="style-label">背景颜色</span>
<div class="input-item">
<span class="input-label">背景颜色</span>
<a-input
class="style-input"
class="input-area"
type="color"
v-model:value="currentBackground"
/>
<div class="style-item-value">{{ currentBackground }}</div>
<div class="input-item-value">{{ currentBackground }}</div>
</div>
<div class="style-item">
<span class="style-label">背景透明度</span>
<div class="input-item">
<span class="input-label">背景透明度</span>
<a-input
class="style-input"
class="input-area"
type="range"
min="0"
max="100"
v-model:value="currentOpacity"
/>
<div class="style-item-value">{{ currentOpacity }}</div>
<div class="input-item-value">{{ currentOpacity }}</div>
</div>
<div class="style-item">
<span class="style-label">显示预览</span>
<div class="input-item">
<span class="input-label">显示预览</span>
<a-switch v-model:checked="displayPreview" />
<span class="style-label">显示翻译</span>
<span class="input-label">显示翻译</span>
<a-switch v-model:checked="currentTransDisplay" />
</div>
@@ -64,31 +64,31 @@
<template #extra>
<a @click="useSameStyle">使用相同样式</a>
</template>
<div class="style-item">
<span class="style-label">翻译字体</span>
<div class="input-item">
<span class="input-label">翻译字体</span>
<a-input
class="style-input"
class="input-area"
v-model:value="currentTransFontFamily"
/>
/>
</div>
<div class="style-item">
<span class="style-label">翻译颜色</span>
<div class="input-item">
<span class="input-label">翻译颜色</span>
<a-input
class="style-input"
class="input-area"
type="color"
v-model:value="currentTransFontColor"
/>
<div class="style-item-value">{{ currentTransFontColor }}</div>
<div class="input-item-value">{{ currentTransFontColor }}</div>
</div>
<div class="style-item">
<span class="style-label">翻译大小</span>
<div class="input-item">
<span class="input-label">翻译大小</span>
<a-input
class="style-input"
class="input-area"
type="range"
min="0" max="64"
v-model:value="currentTransFontSize"
/>
<div class="style-item-value">{{ currentTransFontSize }}px</div>
/>
<div class="input-item-value">{{ currentTransFontSize }}px</div>
</div>
</a-card>
</div>
@@ -115,10 +115,10 @@
:style="{
fontFamily: currentTransFontFamily,
fontSize: currentTransFontSize + 'px',
color: currentTransFontColor
color: currentTransFontColor
}"
>这是字幕样式预览(翻译)</p>
</div>
</div>
</Teleport>
</template>
@@ -154,7 +154,7 @@ function useSameStyle(){
currentTransFontColor.value = currentFontColor.value;
}
function applyStyle(){
function applyStyle(){
captionStyle.fontFamily = currentFontFamily.value;
captionStyle.fontSize = currentFontSize.value;
captionStyle.fontColor = currentFontColor.value;
@@ -182,7 +182,7 @@ function backStyle(){
currentTransFontColor.value = captionStyle.transFontColor;
}
function resetStyle() {
function resetStyle() {
captionStyle.sendStyleReset();
}
@@ -195,33 +195,7 @@ watch(changeSignal, (val) => {
</script>
<style scoped>
.caption-button {
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
}
@import url(../assets/input.css);
.preview-container {
line-height: 2em;
@@ -239,4 +213,4 @@ watch(changeSignal, (val) => {
margin: 0;
line-height: 1.5em;
}
</style>
</style>

View File

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

View File

@@ -10,6 +10,10 @@
<a-col :span="6">
<a-statistic title="已记录字幕" :value="captionData.length" />
</a-col>
<a-col :span="6">
<div class="about-tag">关于本项目</div>
<GithubOutlined class="proj-info" @click="showAbout = true"/>
</a-col>
</a-row>
</div>
@@ -28,12 +32,45 @@
@click="stopEngine"
>关闭字幕引擎</a-button>
</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>
<script setup lang="ts">
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useCaptionLogStore } from '@renderer/stores/captionLog'
import { useEngineControlStore } from '@renderer/stores/engineControl'
import { GithubOutlined } from '@ant-design/icons-vue';
const showAbout = ref(false)
const captionLog = useCaptionLogStore()
const { captionData } = storeToRefs(captionLog)
const engineControl = useEngineControlStore()
@@ -53,6 +90,48 @@ function stopEngine() {
</script>
<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 {
display: flex;
flex-wrap: wrap;

View File

@@ -1,53 +1,29 @@
<template>
<a-card size="small" title="页面宽度">
<template #extra>
<a-button type="link" @click="showAbout = true">关于本项目</a-button>
</template>
<a-card size="small" title="通用设置">
<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>
</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>
<script setup lang="ts">
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useGeneralSettingStore } from '@renderer/stores/generalSetting'
const generalSettingStore = useGeneralSettingStore()
const { leftBarWidth } = storeToRefs(generalSettingStore)
const showAbout = ref(false)
</script>
<style scoped>
@import url(../assets/input.css);
.span-input {
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 router from './router'
import i18n from './i18n'
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/reset.css';
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(i18n)
app.use(Antd)
app.mount('#app')

View File

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

View File

@@ -44,34 +44,4 @@ const { leftBarWidth } = storeToRefs(generalSettingStore)
overflow-y: auto;
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>