新增i18n
This commit is contained in:
parent
42d233c0d7
commit
bba78d1dca
67
package-lock.json
generated
67
package-lock.json
generated
@ -11,7 +11,8 @@
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.5.13"
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^9.14.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.0.4",
|
||||
@ -525,6 +526,50 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "9.14.4",
|
||||
"resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-9.14.4.tgz",
|
||||
"integrity": "sha512-vtZCt7NqWhKEtHa3SD/322DlgP5uR9MqWxnE0y8Q0tjDs9H5Lxhss+b5wv8rmuXRoHKLESNgw9d+EN9ybBbj9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "9.14.4",
|
||||
"@intlify/shared": "9.14.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "9.14.4",
|
||||
"resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-9.14.4.tgz",
|
||||
"integrity": "sha512-vcyCLiVRN628U38c3PbahrhbbXrckrM9zpy0KZVlDk2Z0OnGwv8uQNNXP3twwGtfLsCf4gu3ci6FMIZnPaqZsw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "9.14.4",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "9.14.4",
|
||||
"resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-9.14.4.tgz",
|
||||
"integrity": "sha512-P9zv6i1WvMc9qDBWvIgKkymjY2ptIiQ065PjDv7z7fDqH3J/HBRBN5IoiR46r/ujRcU7hCuSIZWvCAFCyuOYZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
@ -2848,6 +2893,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "9.14.4",
|
||||
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.14.4.tgz",
|
||||
"integrity": "sha512-B934C8yUyWLT0EMud3DySrwSUJI7ZNiWYsEEz2gknTthqKiG4dzWE/WSa8AzCuSQzwBEv4HtG1jZDhgzPfWSKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "9.14.4",
|
||||
"@intlify/shared": "9.14.4",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-tsc": {
|
||||
"version": "2.2.12",
|
||||
"resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz",
|
||||
|
||||
@ -18,7 +18,8 @@
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.5.13"
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^9.14.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.0.4",
|
||||
|
||||
@ -146,6 +146,11 @@ fn configure_macos_menu(app: &AppHandle) -> tauri::Result<()> {
|
||||
.item(&MenuItemBuilder::with_id("reset_models", "恢复默认模型路径").build(app)?)
|
||||
.build()?;
|
||||
|
||||
let language_menu = SubmenuBuilder::new(app, "语言")
|
||||
.item(&MenuItemBuilder::with_id("set_lang_zh_cn", "中文").build(app)?)
|
||||
.item(&MenuItemBuilder::with_id("set_lang_en", "English").build(app)?)
|
||||
.build()?;
|
||||
|
||||
let window_menu = SubmenuBuilder::new(app, "窗口")
|
||||
.item(&PredefinedMenuItem::minimize(app, None)?)
|
||||
.item(&PredefinedMenuItem::maximize(app, None)?)
|
||||
@ -158,6 +163,7 @@ fn configure_macos_menu(app: &AppHandle) -> tauri::Result<()> {
|
||||
.item(&file_menu)
|
||||
.item(&edit_menu)
|
||||
.item(&settings_menu)
|
||||
.item(&language_menu)
|
||||
.item(&window_menu)
|
||||
.build()?;
|
||||
|
||||
@ -171,6 +177,8 @@ fn configure_macos_menu(app: &AppHandle) -> tauri::Result<()> {
|
||||
"toggle_advanced" => Some("toggle-advanced"),
|
||||
"toggle_bilingual" => Some("toggle-bilingual"),
|
||||
"reset_models" => Some("reset-models"),
|
||||
"set_lang_zh_cn" => Some("set-lang-zh-CN"),
|
||||
"set_lang_en" => Some("set-lang-en"),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
|
||||
80
src/App.vue
80
src/App.vue
@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||
@ -8,10 +9,8 @@ import SubtitleEditor from './components/SubtitleEditor.vue'
|
||||
import { useTaskStore } from './stores/tasks'
|
||||
import type { DefaultModelPaths, OutputMode, TargetLanguage, TranslationConfig } from './lib/types'
|
||||
|
||||
const DEFAULT_WHISPER_MODEL = '应用内置 Whisper 模型'
|
||||
const DEFAULT_VAD_MODEL = '应用内置 VAD 模型'
|
||||
const { t, locale } = useI18n()
|
||||
const SOURCE_LANGUAGE_OPTIONS = [
|
||||
{ value: 'auto', label: '自动识别' },
|
||||
{ value: 'zh', label: '中文' },
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'ja', label: '日本語' },
|
||||
@ -31,7 +30,7 @@ const SOURCE_LANGUAGE_OPTIONS = [
|
||||
{ value: 'uk', label: 'Українська' },
|
||||
{ value: 'pl', label: 'Polski' },
|
||||
{ value: 'nl', label: 'Nederlands' },
|
||||
] as const
|
||||
]
|
||||
const MEDIA_EXTENSIONS = [
|
||||
'mp3',
|
||||
'wav',
|
||||
@ -90,6 +89,10 @@ onUnmounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(locale, (newLocale) => {
|
||||
localStorage.setItem('locale', newLocale)
|
||||
})
|
||||
|
||||
function persistTranslationConfig() {
|
||||
localStorage.setItem('llm.apiBase', translationConfig.value.apiBase)
|
||||
localStorage.setItem('llm.apiKey', translationConfig.value.apiKey)
|
||||
@ -101,7 +104,7 @@ function persistTranslationConfig() {
|
||||
function resetModelPaths() {
|
||||
whisperModelPath.value = defaultModelPaths.value?.whisperModelPath ?? ''
|
||||
vadModelPath.value = defaultModelPaths.value?.vadModelPath ?? ''
|
||||
feedback.value = '已恢复默认模型路径'
|
||||
feedback.value = t('app.feedback.restoredDefaults')
|
||||
}
|
||||
|
||||
async function loadDefaultModelPaths() {
|
||||
@ -116,7 +119,7 @@ async function loadDefaultModelPaths() {
|
||||
}
|
||||
} catch (error) {
|
||||
feedback.value =
|
||||
error instanceof Error ? `读取默认模型路径失败:${error.message}` : '读取默认模型路径失败'
|
||||
error instanceof Error ? `${t('app.feedback.loadModelError')}${error.message}` : t('app.feedback.loadModelErrorFallback')
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,11 +143,17 @@ async function bindMenuActions() {
|
||||
break
|
||||
case 'toggle-bilingual':
|
||||
bilingualOutput.value = !bilingualOutput.value
|
||||
feedback.value = bilingualOutput.value ? '已开启双语导出' : '已关闭双语导出'
|
||||
feedback.value = bilingualOutput.value ? t('app.feedback.bilingualOn') : t('app.feedback.bilingualOff')
|
||||
break
|
||||
case 'reset-models':
|
||||
resetModelPaths()
|
||||
break
|
||||
case 'set-lang-zh-CN':
|
||||
locale.value = 'zh-CN'
|
||||
break
|
||||
case 'set-lang-en':
|
||||
locale.value = 'en'
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -172,12 +181,12 @@ async function submitFiles(filePaths: string[]) {
|
||||
})
|
||||
}
|
||||
if (fallbackToSource) {
|
||||
feedback.value = '未填写 LLM API Key,已自动回退为原文转录'
|
||||
feedback.value = t('app.feedback.fallbackSource')
|
||||
} else {
|
||||
feedback.value = `已提交 ${filePaths.length} 个任务`
|
||||
feedback.value = t('app.feedback.submitted', { count: filePaths.length })
|
||||
}
|
||||
} catch (error) {
|
||||
feedback.value = error instanceof Error ? error.message : '任务提交失败'
|
||||
feedback.value = error instanceof Error ? error.message : t('app.feedback.submitFailed')
|
||||
} finally {
|
||||
pending.value = false
|
||||
}
|
||||
@ -190,10 +199,10 @@ async function handlePickFiles() {
|
||||
const selected = await open({
|
||||
multiple: true,
|
||||
directory: false,
|
||||
title: '选择音视频文件',
|
||||
title: t('app.pickFiles'),
|
||||
filters: [
|
||||
{
|
||||
name: '媒体文件',
|
||||
name: t('app.mediaFiles'),
|
||||
extensions: [...MEDIA_EXTENSIONS],
|
||||
},
|
||||
],
|
||||
@ -206,7 +215,7 @@ async function handlePickFiles() {
|
||||
|
||||
await submitFiles(filePaths)
|
||||
} catch (error) {
|
||||
feedback.value = error instanceof Error ? `打开文件对话框失败:${error.message}` : '打开文件对话框失败'
|
||||
feedback.value = error instanceof Error ? `${t('app.feedback.dialogError')}${error.message}` : t('app.feedback.dialogErrorFallback')
|
||||
}
|
||||
}
|
||||
|
||||
@ -230,7 +239,7 @@ async function handleFiles(event: Event) {
|
||||
async function handleExport(format: 'srt' | 'vtt' | 'ass') {
|
||||
if (!selectedTask.value) return
|
||||
const output = await taskStore.exportTask(selectedTask.value.id, format)
|
||||
feedback.value = `已导出到 ${output}`
|
||||
feedback.value = output
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -246,10 +255,10 @@ async function handleExport(format: 'srt' | 'vtt' | 'ass') {
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<button class="button secondary toggle-button" type="button" @click="showAdvanced = !showAdvanced">
|
||||
{{ showAdvanced ? '收起' : '设置' }}
|
||||
{{ showAdvanced ? $t('app.collapse') : $t('app.settings') }}
|
||||
</button>
|
||||
<button class="button" type="button" :disabled="pending" @click="handlePickFiles">
|
||||
{{ pending ? '提交中...' : '添加任务' }}
|
||||
{{ pending ? $t('app.submitting') : $t('app.addTask') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -258,22 +267,23 @@ async function handleExport(format: 'srt' | 'vtt' | 'ass') {
|
||||
<div class="toolbar-group">
|
||||
<div class="form-grid">
|
||||
<label class="field">
|
||||
<span>模式</span>
|
||||
<span>{{ $t('app.mode') }}</span>
|
||||
<select v-model="outputMode">
|
||||
<option value="source">原文</option>
|
||||
<option value="translate">翻译</option>
|
||||
<option value="source">{{ $t('app.source') }}</option>
|
||||
<option value="translate">{{ $t('app.translate') }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label v-if="outputMode === 'translate'" class="field">
|
||||
<span>目标语言</span>
|
||||
<span>{{ $t('app.targetLang') }}</span>
|
||||
<select v-model="targetLang">
|
||||
<option value="zh">中文</option>
|
||||
<option value="en">英文</option>
|
||||
<option value="zh">{{ $t('app.chinese') }}</option>
|
||||
<option value="en">{{ $t('app.english') }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>源语言</span>
|
||||
<span>{{ $t('app.sourceLang') }}</span>
|
||||
<select v-model="sourceLang">
|
||||
<option value="auto">{{ $t('app.autoDetect') }}</option>
|
||||
<option
|
||||
v-for="option in SOURCE_LANGUAGE_OPTIONS"
|
||||
:key="option.value"
|
||||
@ -285,7 +295,7 @@ async function handleExport(format: 'srt' | 'vtt' | 'ass') {
|
||||
</label>
|
||||
<label class="check desktop-check">
|
||||
<input v-model="bilingualOutput" type="checkbox" />
|
||||
<span>双语导出</span>
|
||||
<span>{{ $t('app.bilingualExport') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@ -294,42 +304,42 @@ async function handleExport(format: 'srt' | 'vtt' | 'ass') {
|
||||
</div>
|
||||
|
||||
<div v-if="showAdvanced" class="advanced-shell">
|
||||
<span class="group-title">高级设置</span>
|
||||
<span class="group-title">{{ $t('app.advanced') }}</span>
|
||||
<div class="advanced-grid">
|
||||
<template v-if="outputMode === 'translate'">
|
||||
<label class="field wide">
|
||||
<span>LLM API Base</span>
|
||||
<span>{{ $t('app.llm.apiBase') }}</span>
|
||||
<input v-model="translationConfig.apiBase" placeholder="https://api.openai.com/v1" />
|
||||
</label>
|
||||
<label class="field wide">
|
||||
<span>LLM API Key</span>
|
||||
<span>{{ $t('app.llm.apiKey') }}</span>
|
||||
<input v-model="translationConfig.apiKey" type="password" placeholder="sk-..." />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>LLM Model</span>
|
||||
<span>{{ $t('app.llm.model') }}</span>
|
||||
<input v-model="translationConfig.model" placeholder="GLM-4-Flash-250414" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>批大小</span>
|
||||
<span>{{ $t('app.llm.batchSize') }}</span>
|
||||
<input v-model.number="translationConfig.batchSize" type="number" min="10" max="15" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>上下文</span>
|
||||
<span>{{ $t('app.llm.contextSize') }}</span>
|
||||
<input v-model.number="translationConfig.contextSize" type="number" min="0" max="5" />
|
||||
</label>
|
||||
</template>
|
||||
<label class="field wide">
|
||||
<span>Whisper 模型</span>
|
||||
<span>{{ $t('app.model.whisper') }}</span>
|
||||
<input
|
||||
v-model="whisperModelPath"
|
||||
:placeholder="defaultModelPaths?.whisperModelPath || DEFAULT_WHISPER_MODEL"
|
||||
:placeholder="defaultModelPaths?.whisperModelPath || $t('app.model.defaultWhisper')"
|
||||
/>
|
||||
</label>
|
||||
<label class="field wide">
|
||||
<span>VAD 模型</span>
|
||||
<span>{{ $t('app.model.vad') }}</span>
|
||||
<input
|
||||
v-model="vadModelPath"
|
||||
:placeholder="defaultModelPaths?.vadModelPath || DEFAULT_VAD_MODEL"
|
||||
:placeholder="defaultModelPaths?.vadModelPath || $t('app.model.defaultVad')"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@ -48,9 +48,9 @@ function updateTranslatedText(segment: SubtitleSegment, value: string) {
|
||||
<section class="panel workspace-panel">
|
||||
<div class="workspace-header">
|
||||
<div>
|
||||
<strong>{{ task?.fileName ?? '字幕工作区' }}</strong>
|
||||
<strong>{{ task?.fileName ?? $t('editor.title') }}</strong>
|
||||
<p class="panel-subtitle">
|
||||
{{ task ? `${segments.length} 条片段` : '选择左侧任务后开始查看' }}
|
||||
{{ task ? $t('editor.segments', { count: segments.length }) : $t('editor.selectTask') }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="task" class="export-actions">
|
||||
@ -61,14 +61,14 @@ function updateTranslatedText(segment: SubtitleSegment, value: string) {
|
||||
</div>
|
||||
|
||||
<div v-if="!task" class="empty-state">
|
||||
<p>选择任务后显示字幕</p>
|
||||
<p style="margin-top: 6px; font-size: 11px;">点击左侧任务列表中的任务开始查看</p>
|
||||
<p>{{ $t('editor.empty') }}</p>
|
||||
<p style="margin-top: 6px; font-size: 11px;">{{ $t('editor.emptyHint') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="segments.length === 0" class="empty-state">
|
||||
<template v-if="isProcessing">正在处理中,请稍候...</template>
|
||||
<template v-else-if="task?.status === 'failed'">任务处理失败,无法生成字幕</template>
|
||||
<template v-else>暂无字幕片段</template>
|
||||
<template v-if="isProcessing">{{ $t('editor.processing') }}</template>
|
||||
<template v-else-if="task?.status === 'failed'">{{ $t('editor.failed') }}</template>
|
||||
<template v-else>{{ $t('editor.noSegments') }}</template>
|
||||
</div>
|
||||
|
||||
<div v-else class="segment-list">
|
||||
@ -81,11 +81,11 @@ function updateTranslatedText(segment: SubtitleSegment, value: string) {
|
||||
<span>{{ formatTime(segment.start) }} - {{ formatTime(segment.end) }}</span>
|
||||
<span class="segment-id">{{ segment.id }}</span>
|
||||
</div>
|
||||
<p class="source-text">{{ segment.sourceText || '等待识别结果...' }}</p>
|
||||
<p class="source-text">{{ segment.sourceText || $t('editor.waiting') }}</p>
|
||||
<textarea
|
||||
class="editor-input"
|
||||
:value="segment.translatedText ?? ''"
|
||||
:placeholder="task.outputMode === 'translate' ? '译文' : '原文'"
|
||||
:placeholder="task.outputMode === 'translate' ? $t('editor.translation') : $t('editor.source')"
|
||||
:disabled="task.outputMode === 'source'"
|
||||
@change="updateTranslatedText(segment, ($event.target as HTMLTextAreaElement).value)"
|
||||
/>
|
||||
@ -94,13 +94,13 @@ function updateTranslatedText(segment: SubtitleSegment, value: string) {
|
||||
|
||||
<div class="log-drawer" :class="{ expanded: logsExpanded }">
|
||||
<button class="log-toggle" type="button" @click="logsExpanded = !logsExpanded">
|
||||
<span>日志</span>
|
||||
<span>{{ $t('editor.logs') }}</span>
|
||||
<span class="subtle">{{ logs.length }}</span>
|
||||
<span class="log-chevron">{{ logsExpanded ? '−' : '+' }}</span>
|
||||
</button>
|
||||
<div v-if="logsExpanded" class="log-panel">
|
||||
<div v-if="logs.length === 0" class="empty-state">
|
||||
暂无日志
|
||||
{{ $t('editor.noLogs') }}
|
||||
</div>
|
||||
<div v-else class="log-list">
|
||||
<pre v-for="(line, index) in logs" :key="`${index}-${line}`" class="log-line">{{ line }}</pre>
|
||||
|
||||
@ -10,30 +10,20 @@ const emit = defineEmits<{
|
||||
select: [taskId: string]
|
||||
retry: [taskId: string]
|
||||
}>()
|
||||
|
||||
const statusLabel: Record<SubtitleTask['status'], string> = {
|
||||
queued: '排队中',
|
||||
extracting: '抽取',
|
||||
vad_processing: 'VAD',
|
||||
transcribing: '识别',
|
||||
translating: '翻译',
|
||||
completed: '完成',
|
||||
failed: '失败',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="panel sidebar-panel">
|
||||
<div class="panel-title">
|
||||
<div>
|
||||
<strong>任务</strong>
|
||||
<p class="panel-subtitle">选择任务查看字幕</p>
|
||||
<strong>{{ $t('taskQueue.title') }}</strong>
|
||||
<p class="panel-subtitle">{{ $t('taskQueue.subtitle') }}</p>
|
||||
</div>
|
||||
<span class="badge">{{ tasks.length }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="tasks.length === 0" class="empty-state">
|
||||
暂无任务
|
||||
{{ $t('taskQueue.empty') }}
|
||||
</div>
|
||||
|
||||
<div v-else class="list-stack">
|
||||
@ -53,14 +43,14 @@ const statusLabel: Record<SubtitleTask['status'], string> = {
|
||||
<span
|
||||
class="subtle"
|
||||
:class="{ 'status-active': task.status !== 'completed' && task.status !== 'failed' && task.status !== 'queued' }"
|
||||
>{{ statusLabel[task.status] }}</span>
|
||||
>{{ $t(`taskQueue.status.${task.status}`) }}</span>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" :style="{ width: `${task.progress}%` }" />
|
||||
</div>
|
||||
<div v-if="task.status === 'failed'" class="failed-footer">
|
||||
<p class="error-text">{{ task.error }}</p>
|
||||
<button class="retry-button" type="button" @click.stop="emit('retry', task.id)">重试</button>
|
||||
<button class="retry-button" type="button" @click.stop="emit('retry', task.id)">{{ $t('taskQueue.retry') }}</button>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
26
src/i18n.ts
Normal file
26
src/i18n.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import zhCN from './locales/zh-CN'
|
||||
import en from './locales/en'
|
||||
|
||||
function detectSystemLocale(): string {
|
||||
const saved = localStorage.getItem('locale')
|
||||
if (saved) return saved
|
||||
|
||||
const lang = navigator.language
|
||||
if (lang.startsWith('zh')) return 'zh-CN'
|
||||
return 'en'
|
||||
}
|
||||
|
||||
const defaultLocale = detectSystemLocale()
|
||||
|
||||
export type Locale = 'zh-CN' | 'en'
|
||||
|
||||
export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: defaultLocale,
|
||||
fallbackLocale: 'en',
|
||||
messages: {
|
||||
'zh-CN': zhCN,
|
||||
en,
|
||||
},
|
||||
})
|
||||
77
src/locales/en.ts
Normal file
77
src/locales/en.ts
Normal file
@ -0,0 +1,77 @@
|
||||
export default {
|
||||
app: {
|
||||
title: 'CrossSubtitle',
|
||||
credit: 'by {author}',
|
||||
collapse: 'Collapse',
|
||||
settings: 'Settings',
|
||||
submitting: 'Submitting...',
|
||||
addTask: 'Add Task',
|
||||
mode: 'Mode',
|
||||
source: 'Source',
|
||||
translate: 'Translate',
|
||||
targetLang: 'Target Language',
|
||||
chinese: 'Chinese',
|
||||
english: 'English',
|
||||
sourceLang: 'Source Language',
|
||||
bilingualExport: 'Bilingual Export',
|
||||
autoDetect: 'Auto Detect',
|
||||
advanced: 'Advanced Settings',
|
||||
feedback: {
|
||||
restoredDefaults: 'Restored default model paths',
|
||||
loadModelError: 'Failed to load default model paths: {message}',
|
||||
loadModelErrorFallback: 'Failed to load default model paths',
|
||||
bilingualOn: 'Bilingual export enabled',
|
||||
bilingualOff: 'Bilingual export disabled',
|
||||
fallbackSource: 'LLM API Key not set, falling back to source transcription',
|
||||
submitted: 'Submitted {count} tasks',
|
||||
submitFailed: 'Task submission failed',
|
||||
dialogError: 'Failed to open file dialog: {message}',
|
||||
dialogErrorFallback: 'Failed to open file dialog',
|
||||
},
|
||||
llm: {
|
||||
apiBase: 'LLM API Base',
|
||||
apiKey: 'LLM API Key',
|
||||
model: 'LLM Model',
|
||||
batchSize: 'Batch Size',
|
||||
contextSize: 'Context Size',
|
||||
},
|
||||
model: {
|
||||
whisper: 'Whisper Model',
|
||||
vad: 'VAD Model',
|
||||
defaultWhisper: 'Built-in Whisper Model',
|
||||
defaultVad: 'Built-in VAD Model',
|
||||
},
|
||||
pickFiles: 'Select Media Files',
|
||||
mediaFiles: 'Media Files',
|
||||
},
|
||||
taskQueue: {
|
||||
title: 'Tasks',
|
||||
subtitle: 'Select a task to view subtitles',
|
||||
empty: 'No tasks',
|
||||
retry: 'Retry',
|
||||
status: {
|
||||
queued: 'Queued',
|
||||
extracting: 'Extracting',
|
||||
vad_processing: 'VAD',
|
||||
transcribing: 'Transcribing',
|
||||
translating: 'Translating',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
},
|
||||
},
|
||||
editor: {
|
||||
title: 'Workspace',
|
||||
segments: '{count} segments',
|
||||
selectTask: 'Select a task from the left to start',
|
||||
empty: 'Select a task to view subtitles',
|
||||
emptyHint: 'Click a task in the list to start viewing',
|
||||
processing: 'Processing, please wait...',
|
||||
failed: 'Task failed, unable to generate subtitles',
|
||||
noSegments: 'No subtitle segments',
|
||||
waiting: 'Waiting for transcription...',
|
||||
translation: 'Translation',
|
||||
source: 'Source',
|
||||
logs: 'Logs',
|
||||
noLogs: 'No logs',
|
||||
},
|
||||
}
|
||||
77
src/locales/zh-CN.ts
Normal file
77
src/locales/zh-CN.ts
Normal file
@ -0,0 +1,77 @@
|
||||
export default {
|
||||
app: {
|
||||
title: 'CrossSubtitle',
|
||||
credit: 'by {author}',
|
||||
collapse: '收起',
|
||||
settings: '设置',
|
||||
submitting: '提交中...',
|
||||
addTask: '添加任务',
|
||||
mode: '模式',
|
||||
source: '原文',
|
||||
translate: '翻译',
|
||||
targetLang: '目标语言',
|
||||
chinese: '中文',
|
||||
english: '英文',
|
||||
sourceLang: '源语言',
|
||||
bilingualExport: '双语导出',
|
||||
autoDetect: '自动识别',
|
||||
advanced: '高级设置',
|
||||
feedback: {
|
||||
restoredDefaults: '已恢复默认模型路径',
|
||||
loadModelError: '读取默认模型路径失败:{message}',
|
||||
loadModelErrorFallback: '读取默认模型路径失败',
|
||||
bilingualOn: '已开启双语导出',
|
||||
bilingualOff: '已关闭双语导出',
|
||||
fallbackSource: '未填写 LLM API Key,已自动回退为原文转录',
|
||||
submitted: '已提交 {count} 个任务',
|
||||
submitFailed: '任务提交失败',
|
||||
dialogError: '打开文件对话框失败:{message}',
|
||||
dialogErrorFallback: '打开文件对话框失败',
|
||||
},
|
||||
llm: {
|
||||
apiBase: 'LLM API Base',
|
||||
apiKey: 'LLM API Key',
|
||||
model: 'LLM Model',
|
||||
batchSize: '批大小',
|
||||
contextSize: '上下文',
|
||||
},
|
||||
model: {
|
||||
whisper: 'Whisper 模型',
|
||||
vad: 'VAD 模型',
|
||||
defaultWhisper: '应用内置 Whisper 模型',
|
||||
defaultVad: '应用内置 VAD 模型',
|
||||
},
|
||||
pickFiles: '选择音视频文件',
|
||||
mediaFiles: '媒体文件',
|
||||
},
|
||||
taskQueue: {
|
||||
title: '任务',
|
||||
subtitle: '选择任务查看字幕',
|
||||
empty: '暂无任务',
|
||||
retry: '重试',
|
||||
status: {
|
||||
queued: '排队中',
|
||||
extracting: '抽取',
|
||||
vad_processing: 'VAD',
|
||||
transcribing: '识别',
|
||||
translating: '翻译',
|
||||
completed: '完成',
|
||||
failed: '失败',
|
||||
},
|
||||
},
|
||||
editor: {
|
||||
title: '字幕工作区',
|
||||
segments: '{count} 条片段',
|
||||
selectTask: '选择左侧任务后开始查看',
|
||||
empty: '选择任务后显示字幕',
|
||||
emptyHint: '点击左侧任务列表中的任务开始查看',
|
||||
processing: '正在处理中,请稍候...',
|
||||
failed: '任务处理失败,无法生成字幕',
|
||||
noSegments: '暂无字幕片段',
|
||||
waiting: '等待识别结果...',
|
||||
translation: '译文',
|
||||
source: '原文',
|
||||
logs: '日志',
|
||||
noLogs: '暂无日志',
|
||||
},
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import { i18n } from './i18n'
|
||||
import './style.css'
|
||||
|
||||
createApp(App).use(createPinia()).mount('#app')
|
||||
createApp(App).use(createPinia()).use(i18n).mount('#app')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user