Skip to content

Commit da640d1

Browse files
authored
Feature/add multilingual support (#384)
* feat: add multilingual support (English and Russian) - Add i18n infrastructure with vue-i18n - Implement language switcher component - Add Russian (ru-RU) and English (en-US) translations - Configure TDesign locale for proper UI component translation - Replace hardcoded Chinese strings with i18n keys - Support dynamic language switching in all components - Add translations for all UI elements including: - Menu items and navigation - Knowledge base management - Chat interface - Settings and initialization - Authentication pages - Separator options in document splitting This enables users to use the application in Chinese, English, or Russian. * chore: add vue-i18n dependency and fix Input-field i18n integration - Add vue-i18n package to frontend dependencies - Fix Input-field component i18n integration for multilingual support * chore: add PROGRESS_RU.md to .gitignore - Exclude personal progress tracking file from git * rearrange the order of the multilingual languages: Chinese, English, Russian * Delete docker-compose.yml * Replaced hardcoded messages with the t() function in the following files: all error messages, 14 console.error ,messages session creation messages , login/registration errors * fix: restore docker-compose.yml and update .gitignore * restore docker-compose.yml latest * add multilingual support
1 parent 1fd2de5 commit da640d1

File tree

27 files changed

+2385
-534
lines changed

27 files changed

+2385
-534
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ node_modules/
2424
tmp/
2525
temp/
2626

27+
# Docker compose файл (локальные настройки)
28+
# docker-compose.yml
29+
2730
WeKnora
2831
/models/
2932
services/docreader/src/proto/__pycache__
@@ -36,3 +39,4 @@ data/files/
3639
### macOS
3740
# General
3841
.DS_Store
42+
PROGRESS_RU.md

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"tdesign-icons-vue-next": "^0.4.1",
2323
"tdesign-vue-next": "^1.11.5",
2424
"vue": "^3.5.13",
25+
"vue-i18n": "^9.9.0",
2526
"vue-router": "^4.5.0",
2627
"webpack": "^5.94.0"
2728
},

frontend/src/App.vue

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,31 @@
11
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
import { useI18n } from 'vue-i18n'
4+
import { ConfigProvider } from 'tdesign-vue-next'
5+
import enUS from 'tdesign-vue-next/es/locale/en_US'
6+
import zhCN from 'tdesign-vue-next/es/locale/zh_CN'
7+
import ruRU from 'tdesign-vue-next/es/locale/ru_RU'
8+
9+
const { locale } = useI18n()
10+
11+
const tdesignLocale = computed(() => {
12+
switch (locale.value) {
13+
case 'en-US':
14+
return enUS
15+
case 'ru-RU':
16+
return ruRU
17+
case 'zh-CN':
18+
default:
19+
return zhCN
20+
}
21+
})
222
</script>
323
<template>
4-
<div id="app">
5-
<RouterView />
6-
</div>
24+
<ConfigProvider :global-config="tdesignLocale">
25+
<div id="app">
26+
<RouterView />
27+
</div>
28+
</ConfigProvider>
729
</template>
830
<style>
931
body,

frontend/src/components/Input-field.vue

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
<script setup lang="ts">
22
import { ref, defineEmits, onMounted, defineProps, defineExpose } from "vue";
3+
import { useI18n } from 'vue-i18n';
34
import useKnowledgeBase from '@/hooks/useKnowledgeBase';
45
import { onBeforeRouteUpdate } from 'vue-router';
56
import { MessagePlugin } from "tdesign-vue-next";
7+
8+
const { t } = useI18n();
69
let { cardList, total, getKnowled } = useKnowledgeBase()
710
let query = ref("");
811
const props = defineProps({
@@ -17,15 +20,15 @@ onMounted(() => {
1720
const emit = defineEmits(['send-msg']);
1821
const createSession = (val: string) => {
1922
if (!val.trim()) {
20-
MessagePlugin.info("请先输入内容!");
23+
MessagePlugin.info(t('chat.pleaseEnterContent'));
2124
return
2225
}
2326
if (!query.value && cardList.value.length == 0) {
24-
MessagePlugin.info("请先上传知识库!");
27+
MessagePlugin.info(t('chat.pleaseUploadKnowledgeBase'));
2528
return;
2629
}
2730
if (props.isReplying) {
28-
return MessagePlugin.error("正在回复中,请稍后再试!");
31+
return MessagePlugin.error(t('chat.replyingPleaseWait'));
2932
}
3033
emit('send-msg', val);
3134
clearvalue();
@@ -50,9 +53,9 @@ onBeforeRouteUpdate((to, from, next) => {
5053
</script>
5154
<template>
5255
<div class="answers-input">
53-
<t-textarea v-model="query" placeholder="基于知识库提问" name="description" :autosize="true" @keydown="onKeydown" />
56+
<t-textarea v-model="query" :placeholder="t('chat.askKnowledgeBase')" name="description" :autosize="true" @keydown="onKeydown" />
5457
<div class="answers-input-source">
55-
<span>{{ total }}个来源</span>
58+
<span>{{ t('chat.sourcesCount', { count: total }) }}</span>
5659
</div>
5760
<div @click="createSession(query)" class="answers-input-send"
5861
:class="[query.length && total ? '' : 'grey-out']">
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<template>
2+
<div class="language-switcher">
3+
<t-select
4+
v-model="selectedLanguage"
5+
:options="languageOptions"
6+
@change="handleLanguageChange"
7+
:popup-props="{ overlayClassName: 'language-select-popup' }"
8+
size="small"
9+
>
10+
<template #prefixIcon>
11+
<t-icon name="translate" />
12+
</template>
13+
</t-select>
14+
</div>
15+
</template>
16+
17+
<script setup lang="ts">
18+
import { ref, watch } from 'vue'
19+
import { useI18n } from 'vue-i18n'
20+
21+
const { locale } = useI18n()
22+
23+
const languageOptions = [
24+
{ label: '中文', value: 'zh-CN' },
25+
{ label: 'English', value: 'en-US' },
26+
{ label: 'Русский', value: 'ru-RU' }
27+
]
28+
29+
const selectedLanguage = ref(localStorage.getItem('locale') || 'zh-CN')
30+
31+
const handleLanguageChange = (value: string) => {
32+
console.log('Язык изменен на:', value)
33+
if (value && ['ru-RU', 'en-US', 'zh-CN'].includes(value)) {
34+
locale.value = value
35+
localStorage.setItem('locale', value)
36+
// Перезагрузка страницы для применения нового языка
37+
setTimeout(() => {
38+
window.location.reload()
39+
}, 100)
40+
}
41+
}
42+
43+
// Синхронизация с i18n при инициализации
44+
watch(() => locale.value, (newLocale) => {
45+
if (selectedLanguage.value !== newLocale) {
46+
selectedLanguage.value = newLocale
47+
}
48+
}, { immediate: true })
49+
</script>
50+
51+
<style lang="less" scoped>
52+
.language-switcher {
53+
.t-button {
54+
color: #666;
55+
font-size: 14px;
56+
57+
&:hover {
58+
color: #333;
59+
background-color: rgba(0, 0, 0, 0.04);
60+
}
61+
}
62+
63+
.t-icon {
64+
margin-right: 4px;
65+
}
66+
}
67+
</style>

frontend/src/components/empty-knowledge.vue

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
<script setup lang="ts">
2+
import { useI18n } from 'vue-i18n'
3+
4+
const { t } = useI18n()
25
</script>
36
<template>
47
<div class="empty">
58
<img class="empty-img" src="@/assets/img/upload.svg" alt="">
6-
<span class="empty-txt">知识为空,拖放上传</span>
7-
<span class="empty-type-txt">pdf、doc 格式文件,不超过10M</span>
8-
<span class="empty-type-txt">text、markdown格式文件,不超过200K</span>
9+
<span class="empty-txt">{{ t('knowledgeBase.emptyKnowledgeDragDrop') }}</span>
10+
<span class="empty-type-txt">{{ t('knowledgeBase.pdfDocFormat') }}</span>
11+
<span class="empty-type-txt">{{ t('knowledgeBase.textMarkdownFormat') }}</span>
912
</div>
1013
</template>
1114
<style scoped lang="less">

frontend/src/components/menu.vue

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<div class="menu_icon">
1515
<img class="icon" :src="getImgSrc(item.icon == 'zhishiku' ? knowledgeIcon : item.icon == 'logout' ? logoutIcon : item.icon == 'tenant' ? tenantIcon : prefixIcon)" alt="">
1616
</div>
17-
<span class="menu_title" :title="item.path === 'knowledge-bases' && kbMenuItem ? kbMenuItem.title : item.title">{{ item.path === 'knowledge-bases' && kbMenuItem ? kbMenuItem.title : item.title }}</span>
17+
<span class="menu_title" :title="item.path === 'knowledge-bases' && kbMenuItem?.title ? kbMenuItem.title : t(item.titleKey)">{{ item.path === 'knowledge-bases' && kbMenuItem?.title ? kbMenuItem.title : t(item.titleKey) }}</span>
1818
<!-- 知识库切换下拉箭头 -->
1919
<div v-if="item.path === 'knowledge-bases' && isInKnowledgeBase"
2020
class="kb-dropdown-icon"
@@ -39,7 +39,7 @@
3939
{{ kb.name }}
4040
</div>
4141
</div>
42-
<t-popup overlayInnerClassName="upload-popup" class="placement top center" content="上传知识"
42+
<t-popup overlayInnerClassName="upload-popup" class="placement top center" :content="t('menu.uploadKnowledge')"
4343
placement="top" show-arrow destroy-on-close>
4444
<div class="upload-file-wrap" @click.stop="uploadFile" variant="outline"
4545
v-if="item.path === 'knowledge-bases' && $route.name === 'knowledgeBaseDetail'">
@@ -66,21 +66,21 @@
6666
<t-icon name="ellipsis" class="menu-more" />
6767
</div>
6868
<template #content>
69-
<span class="del_submenu">删除记录</span>
69+
<span class="del_submenu">{{ t('menu.deleteRecord') }}</span>
7070
</template>
7171
</t-popup>
7272
</div>
7373
</div>
7474
</div>
7575
</div>
7676
</div>
77-
77+
7878
<!-- 下半部分:账户信息、系统设置、退出登录 -->
7979
<div class="menu_bottom">
8080
<div class="menu_box" v-for="(item, index) in bottomMenuItems" :key="'bottom-' + index">
8181
<div v-if="item.path === 'logout'">
82-
<t-popconfirm
83-
content="确定要退出登录吗?"
82+
<t-popconfirm
83+
:content="t('menu.confirmLogout')"
8484
@confirm="handleLogout"
8585
placement="top"
8686
:show-arrow="true"
@@ -91,7 +91,7 @@
9191
<div class="menu_icon">
9292
<img class="icon" :src="getImgSrc(logoutIcon)" alt="">
9393
</div>
94-
<span class="menu_title">{{ item.title }}</span>
94+
<span class="menu_title">{{ t(item.titleKey) }}</span>
9595
</div>
9696
</div>
9797
</t-popconfirm>
@@ -103,7 +103,7 @@
103103
<div class="menu_icon">
104104
<img class="icon" :src="getImgSrc(item.icon == 'zhishiku' ? knowledgeIcon : item.icon == 'tenant' ? tenantIcon : prefixIcon)" alt="">
105105
</div>
106-
<span class="menu_title">{{ item.path === 'knowledge-bases' && kbMenuItem ? kbMenuItem.title : item.title }}</span>
106+
<span class="menu_title">{{ item.path === 'knowledge-bases' && kbMenuItem?.title ? kbMenuItem.title : t(item.titleKey) }}</span>
107107
</div>
108108
</div>
109109
</div>
@@ -118,12 +118,14 @@
118118
import { storeToRefs } from 'pinia';
119119
import { onMounted, watch, computed, ref, reactive, nextTick } from 'vue';
120120
import { useRoute, useRouter } from 'vue-router';
121+
import { useI18n } from 'vue-i18n';
121122
import { getSessionsList, delSession } from "@/api/chat/index";
122123
import { getKnowledgeBaseById, listKnowledgeBases, uploadKnowledgeFile } from '@/api/knowledge-base';
123124
import { kbFileTypeVerification } from '@/utils/index';
124125
import { useMenuStore } from '@/stores/menu';
125126
import { useAuthStore } from '@/stores/auth';
126127
import { MessagePlugin } from "tdesign-vue-next";
128+
const { t } = useI18n();
127129
let uploadInput = ref();
128130
const usemenuStore = useMenuStore();
129131
const authStore = useAuthStore();
@@ -234,14 +236,14 @@ const uploadFile = async () => {
234236
const kb = kbResponse.data;
235237
236238
// 检查知识库是否已初始化(有 EmbeddingModelID 和 SummaryModelID)
237-
if (!kb.embedding_model_id || kb.embedding_model_id === '' ||
239+
if (!kb.embedding_model_id || kb.embedding_model_id === '' ||
238240
!kb.summary_model_id || kb.summary_model_id === '') {
239-
MessagePlugin.warning("该知识库尚未完成初始化配置,请先前往设置页面配置模型信息后再上传文件");
241+
MessagePlugin.warning(t('knowledgeBase.notInitialized'));
240242
return;
241243
}
242244
} catch (error) {
243245
console.error('获取知识库信息失败:', error);
244-
MessagePlugin.error("获取知识库信息失败,无法上传文件");
246+
MessagePlugin.error(t('knowledgeBase.getInfoFailed'));
245247
return;
246248
}
247249
}
@@ -260,7 +262,7 @@ const upload = async (e: any) => {
260262
// 获取当前知识库ID
261263
const currentKbId = (route.params as any)?.kbId as string;
262264
if (!currentKbId) {
263-
MessagePlugin.error("缺少知识库ID");
265+
MessagePlugin.error(t('knowledgeBase.missingId'));
264266
return;
265267
}
266268
@@ -280,24 +282,24 @@ const upload = async (e: any) => {
280282
const isSuccess = responseData.success || responseData.code === 200 || responseData.status === 'success' || (!responseData.error && responseData);
281283
282284
if (isSuccess) {
283-
MessagePlugin.info("上传成功!");
285+
MessagePlugin.info(t('file.uploadSuccess'));
284286
} else {
285287
// 改进错误信息提取逻辑
286-
let errorMessage = "上传失败!";
288+
let errorMessage = t('file.uploadFailed');
287289
if (responseData.error && responseData.error.message) {
288290
errorMessage = responseData.error.message;
289291
} else if (responseData.message) {
290292
errorMessage = responseData.message;
291293
}
292294
if (responseData.code === 'duplicate_file' || (responseData.error && responseData.error.code === 'duplicate_file')) {
293-
errorMessage = "文件已存在";
295+
errorMessage = t('file.fileExists');
294296
}
295297
MessagePlugin.error(errorMessage);
296298
}
297299
} catch (err: any) {
298-
let errorMessage = "上传失败!";
300+
let errorMessage = t('file.uploadFailed');
299301
if (err.code === 'duplicate_file') {
300-
errorMessage = "文件已存在";
302+
errorMessage = t('file.fileExists');
301303
} else if (err.error && err.error.message) {
302304
errorMessage = err.error.message;
303305
} else if (err.message) {
@@ -331,7 +333,7 @@ const delCard = (index: number, item: any) => {
331333
}
332334
}
333335
} else {
334-
MessagePlugin.error("删除失败,请稍后再试!");
336+
MessagePlugin.error(t('knowledgeBase.deleteFailed'));
335337
}
336338
})
337339
}
@@ -392,7 +394,7 @@ const getMessageList = async () => {
392394
// 过滤出当前知识库的会话
393395
const filtered = res.data.filter((s: any) => s.knowledge_base_id === kbId)
394396
filtered.forEach((item: any) => {
395-
let obj = { title: item.title ? item.title : "新会话", path: `chat/${kbId}/${item.id}`, id: item.id, isMore: false, isNoTitle: item.title ? false : true }
397+
let obj = { title: item.title ? item.title : t('menu.newSession'), path: `chat/${kbId}/${item.id}`, id: item.id, isMore: false, isNoTitle: item.title ? false : true }
396398
usemenuStore.updatemenuArr(obj)
397399
});
398400
loading.value = false;

frontend/src/i18n/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { createI18n } from 'vue-i18n'
2+
import zhCN from './locales/zh-CN.ts'
3+
import ruRU from './locales/ru-RU.ts'
4+
import enUS from './locales/en-US.ts'
5+
6+
const messages = {
7+
'zh-CN': zhCN,
8+
'en-US': enUS,
9+
'ru-RU': ruRU
10+
}
11+
12+
// Получаем сохраненный язык из localStorage или используем китайский по умолчанию
13+
const savedLocale = localStorage.getItem('locale') || 'zh-CN'
14+
console.log('i18n инициализация с языком:', savedLocale)
15+
16+
const i18n = createI18n({
17+
legacy: false,
18+
locale: savedLocale,
19+
fallbackLocale: 'zh-CN',
20+
globalInjection: true,
21+
messages
22+
})
23+
24+
export default i18n

0 commit comments

Comments
 (0)