feat(i18n): 实现前端国际化

- 新增英文、日文和中文翻译文件
- 添加语言切换功能
- 更新各组件的文本内容以支持国际化
This commit is contained in:
himeditator
2025-07-03 23:29:10 +08:00
parent d608bf59c7
commit 0a10068b38
12 changed files with 348 additions and 77 deletions

View File

@@ -9,6 +9,11 @@
margin-right: 10px;
}
.switch-label {
display: inline-block;
margin-right: 10px;
}
.input-area {
width: calc(100% - 100px);
min-width: 100px;

View File

@@ -0,0 +1,3 @@
/* :root {
} */

View File

@@ -1,6 +1,6 @@
body {
margin: 0;
padding: 0;
height: 100vh;
overflow: hidden;
}
margin: 0;
padding: 0;
height: 100vh;
overflow: hidden;
}

View File

@@ -1,20 +1,20 @@
<template>
<div class="caption-list">
<div class="caption-title">
<span style="margin-right: 30px;">字幕记录</span>
<span style="margin-right: 30px;">{{ $t('log.title') }}</span>
<a-button
type="primary"
style="margin-right: 20px;"
@click="exportCaptions"
:disabled="captionData.length === 0"
>
导出字幕记录
{{ $t('log.export') }}
</a-button>
<a-button
danger
@click="clearCaptions"
>
清空字幕记录
{{ $t('log.clear') }}
</a-button>
</div>
<a-table
@@ -54,7 +54,7 @@ const pagination = ref({
pageSize: 10,
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50'],
showTotal: (total: number) => ` ${total} 条记录`,
showTotal: (total: number) => `Total: ${total}`,
onChange: (page: number, pageSize: number) => {
pagination.value.current = page
pagination.value.pageSize = pageSize
@@ -67,19 +67,19 @@ const pagination = ref({
const columns = [
{
title: '序号',
title: 'index',
dataIndex: 'index',
key: 'index',
width: 80,
},
{
title: '时间',
title: 'time',
dataIndex: 'time',
key: 'time',
width: 160,
},
{
title: '字幕内容',
title: 'content',
dataIndex: 'content',
key: 'content',
},

View File

@@ -1,19 +1,19 @@
<template>
<a-card size="small" title="字幕样式设置">
<a-card size="small" :title="$t('style.title')">
<template #extra>
<a @click="applyStyle">应用样式</a> |
<a @click="backStyle">取消更改</a> |
<a @click="resetStyle">恢复默认</a>
<a @click="applyStyle">{{ $t('style.applyStyle') }}</a> |
<a @click="backStyle">{{ $t('style.cancelChange') }}</a> |
<a @click="resetStyle">{{ $t('style.resetStyle') }}</a>
</template>
<div class="input-item">
<span class="input-label">字体族</span>
<span class="input-label">{{ $t('style.fontFamily') }}</span>
<a-input
class="input-area"
v-model:value="currentFontFamily"
/>
</div>
<div class="input-item">
<span class="input-label">字体颜色</span>
<span class="input-label">{{ $t('style.fontColor') }}</span>
<a-input
class="input-area"
type="color"
@@ -22,7 +22,7 @@
<div class="input-item-value">{{ currentFontColor }}</div>
</div>
<div class="input-item">
<span class="input-label">字体大小</span>
<span class="input-label">{{ $t('style.fontSize') }}</span>
<a-input
class="input-area"
type="range"
@@ -32,7 +32,7 @@
<div class="input-item-value">{{ currentFontSize }}px</div>
</div>
<div class="input-item">
<span class="input-label">背景颜色</span>
<span class="input-label">{{ $t('style.background') }}</span>
<a-input
class="input-area"
type="color"
@@ -41,7 +41,7 @@
<div class="input-item-value">{{ currentBackground }}</div>
</div>
<div class="input-item">
<span class="input-label">背景透明度</span>
<span class="input-label">{{ $t('style.opacity') }}</span>
<a-input
class="input-area"
type="range"
@@ -49,30 +49,33 @@
max="100"
v-model:value="currentOpacity"
/>
<div class="input-item-value">{{ currentOpacity }}</div>
<div class="input-item-value">{{ currentOpacity }}%</div>
</div>
<div class="input-item">
<span class="input-label">显示预览</span>
<span class="input-label">{{ $t('style.preview') }}</span>
<a-switch v-model:checked="displayPreview" />
<span class="input-label">显示翻译</span>
<a-switch v-model:checked="currentTransDisplay" />
<sapn style="display:inline-block;width:20px;"></sapn>
<div style="display: inline-block;">
<span class="switch-label">{{ $t('style.translation') }}</span>
<a-switch v-model:checked="currentTransDisplay" />
</div>
</div>
<div v-show="currentTransDisplay">
<a-card size="small" title="翻译样式设置">
<a-card size="small" :title="$t('style.trans.title')">
<template #extra>
<a @click="useSameStyle">使用相同样式</a>
<a @click="useSameStyle">{{ $t('style.trans.useSame') }}</a>
</template>
<div class="input-item">
<span class="input-label">翻译字体</span>
<span class="input-label">{{ $t('style.fontFamily') }}</span>
<a-input
class="input-area"
v-model:value="currentTransFontFamily"
/>
</div>
<div class="input-item">
<span class="input-label">翻译颜色</span>
<span class="input-label">{{ $t('style.fontColor') }}</span>
<a-input
class="input-area"
type="color"
@@ -81,7 +84,7 @@
<div class="input-item-value">{{ currentTransFontColor }}</div>
</div>
<div class="input-item">
<span class="input-label">翻译大小</span>
<span class="input-label">{{ $t('style.fontSize') }}</span>
<a-input
class="input-area"
type="range"
@@ -92,7 +95,6 @@
</div>
</a-card>
</div>
</a-card>
<Teleport to="body">
@@ -109,7 +111,7 @@
fontSize: currentFontSize + 'px',
color: currentFontColor
}">
{{ "This is a preview of subtitle styles." }}
{{ $t('example.original') }}
</p>
<p class="preview-translation" v-if="currentTransDisplay"
:style="{
@@ -117,7 +119,7 @@
fontSize: currentTransFontSize + 'px',
color: currentTransFontColor
}"
>这是字幕样式预览(翻译)</p>
>{{ $t('example.translation') }}</p>
</div>
</Teleport>

View File

@@ -1,12 +1,12 @@
<template>
<div style="height: 20px;"></div>
<a-card size="small" title="字幕控制">
<a-card size="small" :title="$t('engine.title')">
<template #extra>
<a @click="applyChange">更改设置</a> |
<a @click="cancelChange">取消更改</a>
<a @click="applyChange">{{ $t('engine.applyChange') }}</a> |
<a @click="cancelChange">{{ $t('engine.cancelChange') }}</a>
</template>
<div class="input-item">
<span class="input-label">源语言</span>
<span class="input-label">{{ $t('engine.sourceLang') }}</span>
<a-select
class="input-area"
v-model:value="currentSourceLang"
@@ -14,7 +14,7 @@
></a-select>
</div>
<div class="input-item">
<span class="input-label">翻译语言</span>
<span class="input-label">{{ $t('engine.transLang') }}</span>
<a-select
class="input-area"
v-model:value="currentTargetLang"
@@ -22,7 +22,7 @@
></a-select>
</div>
<div class="input-item">
<span class="input-label">字幕引擎</span>
<span class="input-label">{{ $t('engine.captionEngine') }}</span>
<a-select
class="input-area"
v-model:value="currentEngine"
@@ -30,7 +30,7 @@
></a-select>
</div>
<div class="input-item">
<span class="input-label">音频选择</span>
<span class="input-label">{{ $t('engine.audioType') }}</span>
<a-select
class="input-area"
v-model:value="currentAudio"
@@ -38,23 +38,33 @@
></a-select>
</div>
<div class="input-item">
<span class="input-label">启用翻译</span>
<span class="input-label">{{ $t('engine.enableTranslation') }}</span>
<a-switch v-model:checked="currentTranslation" />
<span class="input-label">自定义引擎</span>
<a-switch v-model:checked="currentCustomized" />
<sapn style="display:inline-block;width:20px;"></sapn>
<div style="display: inline-block;">
<span class="switch-label">{{ $t('engine.customEngine') }}</span>
<a-switch v-model:checked="currentCustomized" />
</div>
</div>
<div v-show="currentCustomized">
<a-card size="small" title="自定义字幕引擎">
<p class="customize-note">说明允许用户使用自定义字幕引擎提供字幕提供的引擎要能通过 <code>child_process.spawn()</code> 进行启动且需要通过 IPC 与项目 node.js 后端进行通信具体通信接口见后端实现</p>
<a-card size="small" :title="$t('engine.custom.title')">
<template #extra>
<a-popover>
<template #content>
<p class="customize-note">{{ $t('engine.custom.note') }}</p>
</template>
<a><InfoCircleOutlined />{{ $t('engine.custom.attention') }}</a>
</a-popover>
</template>
<div class="input-item">
<span class="input-label">引擎路径</span>
<span class="input-label">{{ $t('engine.custom.app') }}</span>
<a-input
class="input-area"
v-model:value="currentCustomizedApp"
></a-input>
</div>
<div class="input-item">
<span class="input-label">引擎指令</span>
<span class="input-label">{{ $t('engine.custom.command') }}</span>
<a-input
class="input-area"
v-model:value="currentCustomizedCommand"
@@ -71,7 +81,7 @@ import { ref, computed, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useEngineControlStore } from '@renderer/stores/engineControl'
import { notification } from 'ant-design-vue'
import { InfoCircleOutlined } from '@ant-design/icons-vue';
const engineControl = useEngineControlStore()
const { captionEngine, audioType, changeSignal } = storeToRefs(engineControl)
@@ -108,8 +118,8 @@ function applyChange(){
engineControl.sendControlChange()
notification.open({
message: '字幕控制已更改',
description: '如果字幕引擎已经启动,需要关闭后重启才会生效'
message: '字幕控制已更改',
description: '如果字幕引擎已经启动,需要关闭后重启才会生效'
});
}
@@ -137,8 +147,8 @@ watch(changeSignal, (val) => {
@import url(../assets/input.css);
.customize-note {
padding: 0 20px;
padding: 10px 10px 0;
color: red;
font-size: 12px;
max-width: min(40vw, 480px);
}
</style>

View File

@@ -2,16 +2,22 @@
<div class="caption-stat">
<a-row>
<a-col :span="6">
<a-statistic title="字幕引擎" :value="(customized && customizedApp)?'自定义':engine" />
<a-statistic
:title="$t('status.engine')"
:value="(customized && customizedApp)?$t('status.customized'):engine"
/>
</a-col>
<a-col :span="6">
<a-statistic title="字幕引擎状态" :value="engineEnabled?'已启动':'未启动'" />
<a-statistic
:title="$t('status.status')"
:value="engineEnabled?$t('status.started'):$t('status.stopped')"
/>
</a-col>
<a-col :span="6">
<a-statistic title="已记录字幕" :value="captionData.length" />
<a-statistic :title="$t('status.logNumber')" :value="captionData.length" />
</a-col>
<a-col :span="6">
<div class="about-tag">关于本项目</div>
<div class="about-tag">{{ $t('status.aboutProj') }}</div>
<GithubOutlined class="proj-info" @click="showAbout = true"/>
</a-col>
</a-row>
@@ -22,42 +28,51 @@
type="primary"
class="control-button"
@click="openCaptionWindow"
>打开字幕窗口</a-button>
>{{ $t('status.openCaption') }}</a-button>
<a-button
class="control-button"
@click="startEngine"
>启动字幕引擎</a-button>
>{{ engineEnabled ? $t('status.restartEngine') : $t('status.startEngine') }}</a-button>
<!-- TODO 添加重启字幕引擎功能 -->
<a-button
danger class="control-button"
@click="stopEngine"
>关闭字幕引擎</a-button>
>{{ $t('status.stopEngine') }}</a-button>
</div>
<a-modal v-model:open="showAbout" title="关于本项目" :footer="null">
<a-modal v-model:open="showAbout" :title="$t('status.about.title')" :footer="null">
<div class="about-modal-content">
<h2 class="about-title">Auto Caption 项目</h2>
<p class="about-desc">一个跨平台的实时字幕显示软件</p>
<h2 class="about-title">{{ $t('status.about.proj') }}</h2>
<p class="about-desc">{{ $t('status.about.desc') }}</p>
<a-divider />
<div class="about-info">
<p><b>作者</b>HiMeditator</p>
<p><b>版本</b>v0.1.0</p>
<p><b>{{ $t('status.about.version') }}</b><a-tag color="green">v0.1.0</a-tag></p>
<p>
<b>项目地址</b>
<a href="https://github.com/HiMeditator/auto-caption" target="_blank">
GitHub | auto-caption
<b>{{ $t('status.about.author') }}</b>
<a
href="https://github.com/HiMeditator"
target="_blank"
>
<a-tag color="blue">HiMeditator</a-tag>
</a>
</p>
<p>
<b>用户手册</b>
<b>{{ $t('status.about.projLink') }}</b>
<a href="https://github.com/HiMeditator/auto-caption" target="_blank">
<a-tag color="blue">GitHub | auto-caption</a-tag>
</a>
</p>
<p>
<b>{{ $t('status.about.manual') }}</b>
<a
href="https://github.com/HiMeditator/auto-caption/blob/main/assets/user-manual_zh.md"
target="_blank"
>
GitHub | user-manual_zh.md
<a-tag color="blue">GitHub | user-manual_zh.md</a-tag>
</a>
</p>
</div>
<div class="about-date">2026 6 26 </div>
<div class="about-date">{{ $t('status.about.date') }}</div>
</div>
</a-modal>
</template>
@@ -125,6 +140,10 @@ function stopEngine() {
font-size: 1em;
}
.about-info b {
margin-right: 1em;
}
.about-date {
margin-top: 1.5em;
color: #aaa;

View File

@@ -1,8 +1,16 @@
<template>
<a-card size="small" title="通用设置">
<a-card size="small" :title="$t('general.title')">
<div>
<div class="input-item">
<span class="input-label">边栏宽度</span>
<span class="input-label">{{ $t('general.uiLanguage') }}</span>
<a-radio-group v-model:value="uiLanguage">
<a-radio-button value="zh">中文</a-radio-button>
<a-radio-button value="en">English</a-radio-button>
<a-radio-button value="ja">日本語</a-radio-button>
</a-radio-group>
</div>
<div class="input-item">
<span class="input-label">{{ $t('general.barWidth') }}</span>
<a-input
type="range" class="span-input"
min="6" max="12" v-model:value="leftBarWidth"
@@ -18,7 +26,7 @@ import { storeToRefs } from 'pinia'
import { useGeneralSettingStore } from '@renderer/stores/generalSetting'
const generalSettingStore = useGeneralSettingStore()
const { leftBarWidth } = storeToRefs(generalSettingStore)
const { uiLanguage, leftBarWidth } = storeToRefs(generalSettingStore)
</script>
<style scoped>

View File

@@ -1,3 +1,76 @@
export default {
example: {
"original": "这是字幕样式预览。",
"translation": "(Translation) This is a preview of subtitle styles."
},
general: {
"title": "General Settings",
"uiLanguage": "Language",
"barWidth": "Width"
},
engine: {
"title": "Caption Engine Settings",
"applyChange": "Apply Chnages",
"cancelChange": "Cancel Changes",
"sourceLang": "Source",
"transLang": "Translation",
"captionEngine": "Engine",
"audioType": "Audio Type",
"systemOutput": "System Audio Output (Speaker)",
"systemInput": "System Audio Input (Microphone)",
"enableTranslation": "Translation",
"customEngine": "Custom Engine",
custom: {
"title": "Custom Caption Engine",
"attention": "Attention",
"note": "Note: Allows users to provide captions using a custom engine. The provided engine should be able to start via the command line and can specify parameters through command-line instructions. The engine needs to communicate with the node.js backend using standard output. For more information, refer to the project's documentation.",
"app": "Engine Path",
"command": "Command"
}
},
style: {
"title": "Caption Style Settings",
"applyStyle": "Apply",
"cancelChange": "Cancel",
"resetStyle": "Reset",
"fontFamily": "Font Family",
"fontColor": "Font Color",
"fontSize": "Font Size",
"background": "Background",
"opacity": "Opacity",
"preview": "Preview",
"translation": "Show Translation",
trans: {
"title": "Translation Style Settings",
"useSame": "Use Original Style"
}
},
status: {
"engine": "Caption Engine",
"customized": "Customized",
"status": "Engine Status",
"started": "Started",
"stopped": "Not Started",
"logNumber": "Caption Count",
"aboutProj": "About Project",
"openCaption": "Open Caption Window",
"startEngine": "Start Caption Engine",
"restartEngine": "Restart Caption Engine",
"stopEngine": "Stop Caption Engine",
about: {
"title": "About This Project",
"proj": "Auto Caption Project",
"desc": "A cross-platform real-time caption display software supporting multiple languages.",
"version": "Software Version",
"author": "Project Author",
"projLink": "Project Link",
"manual": "User Manual",
"date": "June 26, 2026"
}
},
log: {
"title": "Caption Log",
"export": "Export Caption Log",
"clear": "Clear Caption Log"
}
}

View File

@@ -1,3 +1,76 @@
export default {
example: {
"original": "这是字幕样式预览。",
"translation": "(翻訳)これは字幕のスタイルのプレビューです。"
},
general: {
"title": "一般設定",
"uiLanguage": "言語設定",
"barWidth": "左側の幅"
},
engine: {
"title": "字幕エンジン設定",
"applyChange": "変更を適用",
"cancelChange": "変更をキャンセル",
"sourceLang": "ソース言語",
"transLang": "翻訳言語",
"captionEngine": "エンジン",
"audioType": "オーディオ",
"systemOutput": "システムオーディオ出力(スピーカー)",
"systemInput": "システムオーディオ入力(マイク)",
"enableTranslation": "翻訳",
"customEngine": "カスタムエンジン",
custom: {
"title": "カスタムキャプションエンジン",
"attention": "注意事項",
"note": "注意:ユーザーがカスタムエンジンを使用して字幕を提供できるようにします。提供するエンジンは、コマンドラインから起動でき、パラメータをコマンドラインの指示で指定できる必要があります。エンジンは、標準出力を使用して node.js バックエンドと通信する必要があります。詳細については、プロジェクトドキュメントを参照してください。",
"app": "パス",
"command": "コマンド"
}
},
style: {
"title": "字幕スタイル設定",
"applyStyle": "適用",
"cancelChange": "キャンセル",
"resetStyle": "リセット",
"fontFamily": "フォント",
"fontColor": "カラー",
"fontSize": "サイズ",
"background": "背景色",
"opacity": "不透明度",
"preview": "プレビュー",
"translation": "翻訳表示",
trans: {
"title": "翻訳スタイル設定",
"useSame": "原文のスタイルを使用"
}
},
status: {
"engine": "字幕エンジン",
"customized": "カスタマイズ済み",
"status": "エンジン状態",
"started": "開始済み",
"stopped": "未開始",
"logNumber": "字幕数",
"aboutProj": "プロジェクト情報",
"openCaption": "字幕ウィンドウを開く",
"startEngine": "字幕エンジンを開始",
"restartEngine": "字幕エンジンを再起動",
"stopEngine": "字幕エンジンを停止",
about: {
"title": "このプロジェクトについて",
"proj": "Auto Caption プロジェクト",
"desc": "複数の言語をサポートするクロスプラットフォームのリアルタイム字幕表示ソフトウェア。",
"version": "ソフトウェアバージョン",
"author": "プロジェクト作者",
"projLink": "プロジェクトリンク",
"manual": "ユーザーマニュアル",
"date": "2026 年 6 月 26 日"
}
},
log: {
"title": "字幕ログ",
"export": "エクスポート",
"clear": "字幕ログをクリア"
}
}

View File

@@ -1,8 +1,76 @@
export default {
example: {
"original": "This is a preview of subtitle styles.",
"translation": "这是字幕样式预览(翻译)"
"translation": "(翻译)这是字幕样式预览"
},
general: {
"title": "通用设置",
"uiLanguage": "界面语言",
"barWidth": "左侧宽度"
},
engine: {
"title": "字幕引擎设置",
"applyChange": "应用更改",
"cancelChange": "取消更改",
"sourceLang": "源语言",
"transLang": "翻译语言",
"captionEngine": "字幕引擎",
"audioType": "音频类型",
"systemOutput": "系统音频输出(扬声器)",
"systemInput": "系统音频输入(麦克风)",
"enableTranslation": "启用翻译",
"customEngine": "自定义引擎",
custom: {
"title": "自定义字幕引擎",
"attention": "注意事项",
"note": "说明:允许用户使用自定义引擎提供字幕。提供的引擎要能通过命令行启动,且可以提供命令行指令来指定参数。引擎需要使用标准输出与软件 node.js 后端进行通信。详细信息参考项目文档。",
"app": "引擎路径",
"command": "引擎指令"
}
},
style: {
"title": "字幕样式设置",
"applyStyle": "应用样式",
"cancelChange": "取消更改",
"resetStyle": "恢复默认",
"fontFamily": "字体族",
"fontColor": "字体颜色",
"fontSize": "字体大小",
"background": "背景颜色",
"opacity": "不透明度",
"preview": "显示预览",
"translation": "显示翻译",
trans: {
"title": "翻译样式设置",
"useSame": "使用原文样式"
}
},
status: {
"engine": "字幕引擎",
"customized": "自定义",
"status": "引擎状态",
"started": "已启动",
"stopped": "未启动",
"logNumber": "字幕数量",
"aboutProj": "项目关于",
"openCaption": "打开字幕窗口",
"startEngine": "启动字幕引擎",
"restartEngine": "重启字幕引擎",
"stopEngine": "关闭字幕引擎",
about: {
"title": "关于本项目",
"proj": "Auto Caption 项目",
"desc": "一个跨平台的支持多种语言的实时字幕显示软件。",
"version": "软件版本",
"author": "项目作者",
"projLink": "项目链接",
"manual": "用户手册",
"date": "2026 年 6 月 26 日"
}
},
log: {
"title": "字幕记录",
"export": "导出字幕记录",
"clear": "清空字幕记录"
}
}

View File

@@ -1,9 +1,19 @@
import { ref } from 'vue'
import { ref, watch } from 'vue'
import { defineStore } from 'pinia'
import i18n from '../i18n'
import type { UILanguage } from '../types'
export const useGeneralSettingStore = defineStore('generalSetting', () => {
const uiLanguage = ref<UILanguage>('zh')
const leftBarWidth = ref<number>(8)
watch(uiLanguage, (newValue) => {
i18n.global.locale.value = newValue
console.log(newValue)
})
return {
uiLanguage,
leftBarWidth
}
})