Update translations and add language settings
This commit is contained in:
66
lightrag_webui/src/components/AppSettings.tsx
Normal file
66
lightrag_webui/src/components/AppSettings.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
||||
import Button from '@/components/ui/Button'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { PaletteIcon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function AppSettings() {
|
||||
const [opened, setOpened] = useState<boolean>(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const language = useSettingsStore.use.language()
|
||||
const setLanguage = useSettingsStore.use.setLanguage()
|
||||
|
||||
const theme = useSettingsStore.use.theme()
|
||||
const setTheme = useSettingsStore.use.setTheme()
|
||||
|
||||
const handleLanguageChange = useCallback((value: string) => {
|
||||
setLanguage(value as 'en' | 'zh')
|
||||
}, [setLanguage])
|
||||
|
||||
const handleThemeChange = useCallback((value: string) => {
|
||||
setTheme(value as 'light' | 'dark' | 'system')
|
||||
}, [setTheme])
|
||||
|
||||
return (
|
||||
<Popover open={opened} onOpenChange={setOpened}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9">
|
||||
<PaletteIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="bottom" align="end" className="w-56">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">{t('settings.language')}</label>
|
||||
<Select value={language} onValueChange={handleLanguageChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
<SelectItem value="zh">中文</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">{t('settings.theme')}</label>
|
||||
<Select value={theme} onValueChange={handleThemeChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">{t('settings.light')}</SelectItem>
|
||||
<SelectItem value="dark">{t('settings.dark')}</SelectItem>
|
||||
<SelectItem value="system">{t('settings.system')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
import { createContext, useEffect, useState } from 'react'
|
||||
import { createContext, useEffect } from 'react'
|
||||
import { Theme, useSettingsStore } from '@/stores/settings'
|
||||
|
||||
type ThemeProviderProps = {
|
||||
@@ -21,30 +21,32 @@ const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
||||
* Component that provides the theme state and setter function to its children.
|
||||
*/
|
||||
export default function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(useSettingsStore.getState().theme)
|
||||
const theme = useSettingsStore.use.theme()
|
||||
const setTheme = useSettingsStore.use.setTheme()
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement
|
||||
root.classList.remove('light', 'dark')
|
||||
|
||||
if (theme === 'system') {
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light'
|
||||
root.classList.add(systemTheme)
|
||||
setTheme(systemTheme)
|
||||
return
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
root.classList.remove('light', 'dark')
|
||||
root.classList.add(e.matches ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
root.classList.add(mediaQuery.matches ? 'dark' : 'light')
|
||||
mediaQuery.addEventListener('change', handleChange)
|
||||
|
||||
return () => mediaQuery.removeEventListener('change', handleChange)
|
||||
} else {
|
||||
root.classList.add(theme)
|
||||
}
|
||||
|
||||
root.classList.add(theme)
|
||||
}, [theme])
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
useSettingsStore.getState().setTheme(theme)
|
||||
setTheme(theme)
|
||||
}
|
||||
setTheme
|
||||
}
|
||||
|
||||
return (
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import Button from '@/components/ui/Button'
|
||||
import { SiteInfo } from '@/lib/constants'
|
||||
import ThemeToggle from '@/components/ThemeToggle'
|
||||
import AppSettings from '@/components/AppSettings'
|
||||
import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -67,12 +67,14 @@ export default function SiteHeader() {
|
||||
</div>
|
||||
|
||||
<nav className="flex items-center">
|
||||
<Button variant="ghost" size="icon" side="bottom" tooltip={t('header.projectRepository')}>
|
||||
<a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
|
||||
<GithubIcon className="size-4" aria-hidden="true" />
|
||||
</a>
|
||||
</Button>
|
||||
<ThemeToggle />
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" side="bottom" tooltip={t('header.projectRepository')}>
|
||||
<a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
|
||||
<GithubIcon className="size-4" aria-hidden="true" />
|
||||
</a>
|
||||
</Button>
|
||||
<AppSettings />
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import en from "./locales/en.json";
|
||||
import zh from "./locales/zh.json";
|
||||
import en from './locales/en.json';
|
||||
import zh from './locales/zh.json';
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
@@ -11,8 +11,8 @@ i18n
|
||||
en: { translation: en },
|
||||
zh: { translation: zh }
|
||||
},
|
||||
lng: "en", // default
|
||||
fallbackLng: "en",
|
||||
lng: 'zh', // default
|
||||
fallbackLng: 'zh',
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
}
|
||||
|
@@ -1,4 +1,11 @@
|
||||
{
|
||||
"settings": {
|
||||
"language": "Language",
|
||||
"theme": "Theme",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System"
|
||||
},
|
||||
"header": {
|
||||
"documents": "Documents",
|
||||
"knowledgeGraph": "Knowledge Graph",
|
||||
|
@@ -1,4 +1,11 @@
|
||||
{
|
||||
"settings": {
|
||||
"language": "语言",
|
||||
"theme": "主题",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"system": "系统"
|
||||
},
|
||||
"header": {
|
||||
"documents": "文档",
|
||||
"knowledgeGraph": "知识图谱",
|
||||
@@ -6,41 +13,41 @@
|
||||
"api": "API",
|
||||
"projectRepository": "项目仓库",
|
||||
"themeToggle": {
|
||||
"switchToLight": "切换到亮色主题",
|
||||
"switchToDark": "切换到暗色主题"
|
||||
"switchToLight": "切换到浅色主题",
|
||||
"switchToDark": "切换到深色主题"
|
||||
}
|
||||
},
|
||||
"documentPanel": {
|
||||
"clearDocuments": {
|
||||
"button": "清除",
|
||||
"tooltip": "清除文档",
|
||||
"title": "清除文档",
|
||||
"confirm": "您确定要清除所有文档吗?",
|
||||
"button": "清空",
|
||||
"tooltip": "清空文档",
|
||||
"title": "清空文档",
|
||||
"confirm": "确定要清空所有文档吗?",
|
||||
"confirmButton": "确定",
|
||||
"success": "文档已成功清除",
|
||||
"failed": "清除文档失败:\n{{message}}",
|
||||
"error": "清除文档失败:\n{{error}}"
|
||||
"success": "文档清空成功",
|
||||
"failed": "清空文档失败:\n{{message}}",
|
||||
"error": "清空文档失败:\n{{error}}"
|
||||
},
|
||||
"uploadDocuments": {
|
||||
"button": "上传",
|
||||
"tooltip": "上传文档",
|
||||
"title": "上传文档",
|
||||
"description": "拖放文档到此处或点击浏览。",
|
||||
"uploading": "正在上传 {{name}}: {{percent}}%",
|
||||
"success": "上传成功:\n{{name}} 上传成功",
|
||||
"failed": "上传失败:\n{{name}}\n{{message}}",
|
||||
"error": "上传失败:\n{{name}}\n{{error}}",
|
||||
"description": "拖拽文件到此处或点击浏览",
|
||||
"uploading": "正在上传 {{name}}:{{percent}}%",
|
||||
"success": "上传成功:\n{{name}} 上传完成",
|
||||
"failed": "上传失败:\n{{name}}\n{{message}}",
|
||||
"error": "上传失败:\n{{name}}\n{{error}}",
|
||||
"generalError": "上传失败\n{{error}}",
|
||||
"fileTypes": "支持的文件类型: TXT, MD, DOCX, PDF, PPTX, RTF, ODT, EPUB, HTML, HTM, TEX, JSON, XML, YAML, YML, CSV, LOG, CONF, INI, PROPERTIES, SQL, BAT, SH, C, CPP, PY, JAVA, JS, TS, SWIFT, GO, RB, PHP, CSS, SCSS, LESS"
|
||||
"fileTypes": "支持的文件类型:TXT, MD, DOCX, PDF, PPTX, RTF, ODT, EPUB, HTML, HTM, TEX, JSON, XML, YAML, YML, CSV, LOG, CONF, INI, PROPERTIES, SQL, BAT, SH, C, CPP, PY, JAVA, JS, TS, SWIFT, GO, RB, PHP, CSS, SCSS, LESS"
|
||||
},
|
||||
"documentManager": {
|
||||
"title": "文档管理",
|
||||
"scanButton": "扫描",
|
||||
"scanTooltip": "扫描文档",
|
||||
"uploadedTitle": "已上传文档",
|
||||
"uploadedDescription": "已上传文档及其状态列表。",
|
||||
"emptyTitle": "暂无文档",
|
||||
"emptyDescription": "尚未上传任何文档。",
|
||||
"uploadedDescription": "已上传文档列表及其状态",
|
||||
"emptyTitle": "无文档",
|
||||
"emptyDescription": "还没有上传任何文档",
|
||||
"columns": {
|
||||
"id": "ID",
|
||||
"summary": "摘要",
|
||||
@@ -54,7 +61,7 @@
|
||||
"status": {
|
||||
"completed": "已完成",
|
||||
"processing": "处理中",
|
||||
"pending": "待处理",
|
||||
"pending": "等待中",
|
||||
"failed": "失败"
|
||||
},
|
||||
"errors": {
|
||||
@@ -74,40 +81,37 @@
|
||||
"showNodeLabel": "显示节点标签",
|
||||
"nodeDraggable": "节点可拖动",
|
||||
"showEdgeLabel": "显示边标签",
|
||||
"hideUnselectedEdges": "隐藏未选中边",
|
||||
"hideUnselectedEdges": "隐藏未选中的边",
|
||||
"edgeEvents": "边事件",
|
||||
"maxQueryDepth": "最大查询深度",
|
||||
"minDegree": "最小度数",
|
||||
"maxLayoutIterations": "最大布局迭代次数",
|
||||
"apiKey": "API 密钥",
|
||||
"enterYourAPIkey": "输入您的 API 密钥",
|
||||
"apiKey": "API密钥",
|
||||
"enterYourAPIkey": "输入您的API密钥",
|
||||
"save": "保存",
|
||||
"refreshLayout": "刷新布局"
|
||||
},
|
||||
|
||||
"zoomControl": {
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "缩小",
|
||||
"resetZoom": "重置缩放"
|
||||
},
|
||||
|
||||
"layoutsControl": {
|
||||
"startAnimation": "开始布局动画",
|
||||
"stopAnimation": "停止布局动画",
|
||||
"layoutGraph": "布局图",
|
||||
"layoutGraph": "图布局",
|
||||
"layouts": {
|
||||
"Circular": "环形布局",
|
||||
"Circlepack": "圆形打包布局",
|
||||
"Random": "随机布局",
|
||||
"Noverlaps": "无重叠布局",
|
||||
"Force Directed": "力导向布局",
|
||||
"Force Atlas": "力导向图谱布局"
|
||||
"Circular": "环形",
|
||||
"Circlepack": "圆形打包",
|
||||
"Random": "随机",
|
||||
"Noverlaps": "无重叠",
|
||||
"Force Directed": "力导向",
|
||||
"Force Atlas": "力图"
|
||||
}
|
||||
},
|
||||
|
||||
"fullScreenControl": {
|
||||
"fullScreen": "全屏",
|
||||
"windowed": "窗口模式"
|
||||
"windowed": "窗口"
|
||||
}
|
||||
},
|
||||
"statusIndicator": {
|
||||
@@ -119,17 +123,17 @@
|
||||
"storageInfo": "存储信息",
|
||||
"workingDirectory": "工作目录",
|
||||
"inputDirectory": "输入目录",
|
||||
"llmConfig": "LLM 配置",
|
||||
"llmBinding": "LLM 绑定",
|
||||
"llmBindingHost": "LLM 绑定主机",
|
||||
"llmModel": "LLM 模型",
|
||||
"maxTokens": "最大 Token 数",
|
||||
"llmConfig": "LLM配置",
|
||||
"llmBinding": "LLM绑定",
|
||||
"llmBindingHost": "LLM绑定主机",
|
||||
"llmModel": "LLM模型",
|
||||
"maxTokens": "最大令牌数",
|
||||
"embeddingConfig": "嵌入配置",
|
||||
"embeddingBinding": "嵌入绑定",
|
||||
"embeddingBindingHost": "嵌入绑定主机",
|
||||
"embeddingModel": "嵌入模型",
|
||||
"storageConfig": "存储配置",
|
||||
"kvStorage": "KV 存储",
|
||||
"kvStorage": "KV存储",
|
||||
"docStatusStorage": "文档状态存储",
|
||||
"graphStorage": "图存储",
|
||||
"vectorStorage": "向量存储"
|
||||
@@ -147,90 +151,77 @@
|
||||
"title": "关系",
|
||||
"id": "ID",
|
||||
"type": "类型",
|
||||
"source": "源",
|
||||
"target": "目标",
|
||||
"source": "源节点",
|
||||
"target": "目标节点",
|
||||
"properties": "属性"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索节点...",
|
||||
"message": "以及其它 {count} 项"
|
||||
"message": "还有 {count} 个"
|
||||
},
|
||||
"graphLabels": {
|
||||
"selectTooltip": "选择查询标签",
|
||||
"noLabels": "未找到标签",
|
||||
"label": "标签",
|
||||
"placeholder": "搜索标签...",
|
||||
"andOthers": "以及其它 {count} 个"
|
||||
"andOthers": "还有 {count} 个"
|
||||
}
|
||||
},
|
||||
"retrievePanel": {
|
||||
"chatMessage": {
|
||||
"copyTooltip": "复制到剪贴板",
|
||||
"copyError": "无法复制文本到剪贴板"
|
||||
"copyError": "复制文本到剪贴板失败"
|
||||
},
|
||||
|
||||
"retrieval": {
|
||||
"startPrompt": "在下面输入您的查询以开始检索",
|
||||
"clear": "清除",
|
||||
"startPrompt": "输入查询开始检索",
|
||||
"clear": "清空",
|
||||
"send": "发送",
|
||||
"placeholder": "输入您的查询...",
|
||||
"error": "错误:无法获取响应"
|
||||
"placeholder": "输入查询...",
|
||||
"error": "错误:获取响应失败"
|
||||
},
|
||||
"querySettings": {
|
||||
"parametersTitle": "参数设置",
|
||||
"parametersTitle": "参数",
|
||||
"parametersDescription": "配置查询参数",
|
||||
|
||||
"queryMode": "查询模式",
|
||||
"queryModeTooltip": "选择检索策略:\n• 朴素:不使用高级技术的基本搜索\n• 本地:基于上下文的信息检索\n• 全局:利用全局知识库\n• 混合:结合本地和全局检索\n• 综合:集成知识图谱与向量检索",
|
||||
"queryModeTooltip": "选择检索策略:\n• Naive:基础搜索,无高级技术\n• Local:上下文相关信息检索\n• Global:利用全局知识库\n• Hybrid:结合本地和全局检索\n• Mix:整合知识图谱和向量检索",
|
||||
"queryModeOptions": {
|
||||
"naive": "朴素",
|
||||
"local": "本地",
|
||||
"global": "全局",
|
||||
"hybrid": "混合",
|
||||
"mix": "综合"
|
||||
"mix": "混合"
|
||||
},
|
||||
|
||||
"responseFormat": "响应格式",
|
||||
"responseFormatTooltip": "定义响应格式。例如:\n• 多个段落\n• 单个段落\n• 项目符号",
|
||||
"responseFormatTooltip": "定义响应格式。例如:\n• 多段落\n• 单段落\n• 要点",
|
||||
"responseFormatOptions": {
|
||||
"multipleParagraphs": "多个段落",
|
||||
"singleParagraph": "单个段落",
|
||||
"bulletPoints": "项目符号"
|
||||
"multipleParagraphs": "多段落",
|
||||
"singleParagraph": "单段落",
|
||||
"bulletPoints": "要点"
|
||||
},
|
||||
|
||||
"topK": "Top K 结果数",
|
||||
"topKTooltip": "要检索的前 K 个项目数量。在“本地”模式下表示实体,在“全局”模式下表示关系",
|
||||
"topKPlaceholder": "结果数",
|
||||
|
||||
"maxTokensTextUnit": "文本单元最大 Token 数",
|
||||
"maxTokensTextUnitTooltip": "每个检索到的文本块允许的最大 Token 数",
|
||||
|
||||
"maxTokensGlobalContext": "全局上下文最大 Token 数",
|
||||
"maxTokensGlobalContextTooltip": "在全局检索中为关系描述分配的最大 Token 数",
|
||||
|
||||
"maxTokensLocalContext": "本地上下文最大 Token 数",
|
||||
"maxTokensLocalContextTooltip": "在本地检索中为实体描述分配的最大 Token 数",
|
||||
|
||||
"topK": "Top K结果",
|
||||
"topKTooltip": "检索的顶部项目数。在'local'模式下表示实体,在'global'模式下表示关系",
|
||||
"topKPlaceholder": "结果数量",
|
||||
"maxTokensTextUnit": "文本单元最大令牌数",
|
||||
"maxTokensTextUnitTooltip": "每个检索文本块允许的最大令牌数",
|
||||
"maxTokensGlobalContext": "全局上下文最大令牌数",
|
||||
"maxTokensGlobalContextTooltip": "全局检索中关系描述的最大令牌数",
|
||||
"maxTokensLocalContext": "本地上下文最大令牌数",
|
||||
"maxTokensLocalContextTooltip": "本地检索中实体描述的最大令牌数",
|
||||
"historyTurns": "历史轮次",
|
||||
"historyTurnsTooltip": "在响应上下文中考虑的完整对话轮次(用户-助手对)",
|
||||
"historyTurnsPlaceholder": "历史轮次的数量",
|
||||
|
||||
"historyTurnsTooltip": "响应上下文中考虑的完整对话轮次(用户-助手对)数量",
|
||||
"historyTurnsPlaceholder": "历史轮次数",
|
||||
"hlKeywords": "高级关键词",
|
||||
"hlKeywordsTooltip": "检索时优先考虑的高级关键词。请用逗号分隔",
|
||||
"hlKeywordsTooltip": "检索中优先考虑的高级关键词列表。用逗号分隔",
|
||||
"hlkeywordsPlaceHolder": "输入关键词",
|
||||
|
||||
"llKeywords": "低级关键词",
|
||||
"llKeywordsTooltip": "用于优化检索焦点的低级关键词。请用逗号分隔",
|
||||
|
||||
"onlyNeedContext": "仅需要上下文",
|
||||
"onlyNeedContextTooltip": "如果为 True,则仅返回检索到的上下文,而不会生成回复",
|
||||
|
||||
"onlyNeedPrompt": "仅需要提示",
|
||||
"onlyNeedPromptTooltip": "如果为 True,则仅返回生成的提示,而不会生成回复",
|
||||
|
||||
"llKeywordsTooltip": "用于细化检索重点的低级关键词列表。用逗号分隔",
|
||||
"onlyNeedContext": "仅需上下文",
|
||||
"onlyNeedContextTooltip": "如果为True,仅返回检索到的上下文而不生成响应",
|
||||
"onlyNeedPrompt": "仅需提示",
|
||||
"onlyNeedPromptTooltip": "如果为True,仅返回生成的提示而不产生响应",
|
||||
"streamResponse": "流式响应",
|
||||
"streamResponseTooltip": "如果为 True,则启用流式输出以获得实时响应"
|
||||
"streamResponseTooltip": "如果为True,启用实时流式输出响应"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import { Message, QueryRequest } from '@/api/lightrag'
|
||||
import { useGraphStore } from '@/stores/graph'
|
||||
|
||||
type Theme = 'dark' | 'light' | 'system'
|
||||
type Language = 'en' | 'zh'
|
||||
type Tab = 'documents' | 'knowledge-graph' | 'retrieval' | 'api'
|
||||
|
||||
interface SettingsState {
|
||||
@@ -48,6 +49,9 @@ interface SettingsState {
|
||||
theme: Theme
|
||||
setTheme: (theme: Theme) => void
|
||||
|
||||
language: Language
|
||||
setLanguage: (lang: Language) => void
|
||||
|
||||
enableHealthCheck: boolean
|
||||
setEnableHealthCheck: (enable: boolean) => void
|
||||
|
||||
@@ -59,6 +63,7 @@ const useSettingsStoreBase = create<SettingsState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
theme: 'system',
|
||||
language: 'zh',
|
||||
refreshLayout: () => {
|
||||
const graphState = useGraphStore.getState();
|
||||
const currentGraph = graphState.sigmaGraph;
|
||||
@@ -110,6 +115,13 @@ const useSettingsStoreBase = create<SettingsState>()(
|
||||
|
||||
setTheme: (theme: Theme) => set({ theme }),
|
||||
|
||||
setLanguage: (language: Language) => {
|
||||
import('i18next').then(({ default: i18n }) => {
|
||||
i18n.changeLanguage(language);
|
||||
});
|
||||
set({ language });
|
||||
},
|
||||
|
||||
setGraphLayoutMaxIterations: (iterations: number) =>
|
||||
set({
|
||||
graphLayoutMaxIterations: iterations
|
||||
|
Reference in New Issue
Block a user