Update translations and add language settings

This commit is contained in:
yangdx
2025-03-12 13:49:22 +08:00
parent 19a4163aee
commit 620d31e878
7 changed files with 193 additions and 113 deletions

View 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>
)
}

View File

@@ -1,4 +1,4 @@
import { createContext, useEffect, useState } from 'react' import { createContext, useEffect } from 'react'
import { Theme, useSettingsStore } from '@/stores/settings' import { Theme, useSettingsStore } from '@/stores/settings'
type ThemeProviderProps = { type ThemeProviderProps = {
@@ -21,30 +21,32 @@ const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
* Component that provides the theme state and setter function to its children. * Component that provides the theme state and setter function to its children.
*/ */
export default function ThemeProvider({ children, ...props }: ThemeProviderProps) { 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(() => { useEffect(() => {
const root = window.document.documentElement const root = window.document.documentElement
root.classList.remove('light', 'dark') root.classList.remove('light', 'dark')
if (theme === 'system') { if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
? 'dark' const handleChange = (e: MediaQueryListEvent) => {
: 'light' root.classList.remove('light', 'dark')
root.classList.add(systemTheme) root.classList.add(e.matches ? 'dark' : 'light')
setTheme(systemTheme) }
return
}
root.classList.add(theme) root.classList.add(mediaQuery.matches ? 'dark' : 'light')
mediaQuery.addEventListener('change', handleChange)
return () => mediaQuery.removeEventListener('change', handleChange)
} else {
root.classList.add(theme)
}
}, [theme]) }, [theme])
const value = { const value = {
theme, theme,
setTheme: (theme: Theme) => { setTheme
useSettingsStore.getState().setTheme(theme)
setTheme(theme)
}
} }
return ( return (

View File

@@ -1,6 +1,6 @@
import Button from '@/components/ui/Button' import Button from '@/components/ui/Button'
import { SiteInfo } from '@/lib/constants' import { SiteInfo } from '@/lib/constants'
import ThemeToggle from '@/components/ThemeToggle' import AppSettings from '@/components/AppSettings'
import { TabsList, TabsTrigger } from '@/components/ui/Tabs' import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@@ -67,12 +67,14 @@ export default function SiteHeader() {
</div> </div>
<nav className="flex items-center"> <nav className="flex items-center">
<Button variant="ghost" size="icon" side="bottom" tooltip={t('header.projectRepository')}> <div className="flex items-center gap-2">
<a href={SiteInfo.github} target="_blank" rel="noopener noreferrer"> <Button variant="ghost" size="icon" side="bottom" tooltip={t('header.projectRepository')}>
<GithubIcon className="size-4" aria-hidden="true" /> <a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
</a> <GithubIcon className="size-4" aria-hidden="true" />
</Button> </a>
<ThemeToggle /> </Button>
<AppSettings />
</div>
</nav> </nav>
</header> </header>
) )

View File

@@ -1,8 +1,8 @@
import i18n from "i18next"; import i18n from 'i18next';
import { initReactI18next } from "react-i18next"; import { initReactI18next } from 'react-i18next';
import en from "./locales/en.json"; import en from './locales/en.json';
import zh from "./locales/zh.json"; import zh from './locales/zh.json';
i18n i18n
.use(initReactI18next) .use(initReactI18next)
@@ -11,8 +11,8 @@ i18n
en: { translation: en }, en: { translation: en },
zh: { translation: zh } zh: { translation: zh }
}, },
lng: "en", // default lng: 'zh', // default
fallbackLng: "en", fallbackLng: 'zh',
interpolation: { interpolation: {
escapeValue: false escapeValue: false
} }

View File

@@ -1,4 +1,11 @@
{ {
"settings": {
"language": "Language",
"theme": "Theme",
"light": "Light",
"dark": "Dark",
"system": "System"
},
"header": { "header": {
"documents": "Documents", "documents": "Documents",
"knowledgeGraph": "Knowledge Graph", "knowledgeGraph": "Knowledge Graph",

View File

@@ -1,4 +1,11 @@
{ {
"settings": {
"language": "语言",
"theme": "主题",
"light": "浅色",
"dark": "深色",
"system": "系统"
},
"header": { "header": {
"documents": "文档", "documents": "文档",
"knowledgeGraph": "知识图谱", "knowledgeGraph": "知识图谱",
@@ -6,41 +13,41 @@
"api": "API", "api": "API",
"projectRepository": "项目仓库", "projectRepository": "项目仓库",
"themeToggle": { "themeToggle": {
"switchToLight": "切换到色主题", "switchToLight": "切换到色主题",
"switchToDark": "切换到色主题" "switchToDark": "切换到色主题"
} }
}, },
"documentPanel": { "documentPanel": {
"clearDocuments": { "clearDocuments": {
"button": "清", "button": "清",
"tooltip": "清文档", "tooltip": "清文档",
"title": "清文档", "title": "清文档",
"confirm": "确定要清所有文档吗?", "confirm": "确定要清所有文档吗?",
"confirmButton": "确定", "confirmButton": "确定",
"success": "文档已成功清除", "success": "文档清空成功",
"failed": "清文档失败:\n{{message}}", "failed": "清文档失败\n{{message}}",
"error": "清文档失败:\n{{error}}" "error": "清文档失败\n{{error}}"
}, },
"uploadDocuments": { "uploadDocuments": {
"button": "上传", "button": "上传",
"tooltip": "上传文档", "tooltip": "上传文档",
"title": "上传文档", "title": "上传文档",
"description": "拖放文档到此处或点击浏览", "description": "拖拽文件到此处或点击浏览",
"uploading": "正在上传 {{name}}: {{percent}}%", "uploading": "正在上传 {{name}}{{percent}}%",
"success": "上传成功:\n{{name}} 上传成", "success": "上传成功\n{{name}} 上传成",
"failed": "上传失败:\n{{name}}\n{{message}}", "failed": "上传失败\n{{name}}\n{{message}}",
"error": "上传失败:\n{{name}}\n{{error}}", "error": "上传失败\n{{name}}\n{{error}}",
"generalError": "上传失败\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": { "documentManager": {
"title": "文档管理", "title": "文档管理",
"scanButton": "扫描", "scanButton": "扫描",
"scanTooltip": "扫描文档", "scanTooltip": "扫描文档",
"uploadedTitle": "已上传文档", "uploadedTitle": "已上传文档",
"uploadedDescription": "已上传文档及其状态列表。", "uploadedDescription": "已上传文档列表及其状态",
"emptyTitle": "无文档", "emptyTitle": "无文档",
"emptyDescription": "尚未上传任何文档", "emptyDescription": "还没有上传任何文档",
"columns": { "columns": {
"id": "ID", "id": "ID",
"summary": "摘要", "summary": "摘要",
@@ -54,7 +61,7 @@
"status": { "status": {
"completed": "已完成", "completed": "已完成",
"processing": "处理中", "processing": "处理中",
"pending": "待处理", "pending": "等待中",
"failed": "失败" "failed": "失败"
}, },
"errors": { "errors": {
@@ -74,40 +81,37 @@
"showNodeLabel": "显示节点标签", "showNodeLabel": "显示节点标签",
"nodeDraggable": "节点可拖动", "nodeDraggable": "节点可拖动",
"showEdgeLabel": "显示边标签", "showEdgeLabel": "显示边标签",
"hideUnselectedEdges": "隐藏未选中边", "hideUnselectedEdges": "隐藏未选中边",
"edgeEvents": "边事件", "edgeEvents": "边事件",
"maxQueryDepth": "最大查询深度", "maxQueryDepth": "最大查询深度",
"minDegree": "最小度数", "minDegree": "最小度数",
"maxLayoutIterations": "最大布局迭代次数", "maxLayoutIterations": "最大布局迭代次数",
"apiKey": "API 密钥", "apiKey": "API密钥",
"enterYourAPIkey": "输入您的 API 密钥", "enterYourAPIkey": "输入您的API密钥",
"save": "保存", "save": "保存",
"refreshLayout": "刷新布局" "refreshLayout": "刷新布局"
}, },
"zoomControl": { "zoomControl": {
"zoomIn": "放大", "zoomIn": "放大",
"zoomOut": "缩小", "zoomOut": "缩小",
"resetZoom": "重置缩放" "resetZoom": "重置缩放"
}, },
"layoutsControl": { "layoutsControl": {
"startAnimation": "开始布局动画", "startAnimation": "开始布局动画",
"stopAnimation": "停止布局动画", "stopAnimation": "停止布局动画",
"layoutGraph": "布局", "layoutGraph": "布局",
"layouts": { "layouts": {
"Circular": "环形布局", "Circular": "环形",
"Circlepack": "圆形打包布局", "Circlepack": "圆形打包",
"Random": "随机布局", "Random": "随机",
"Noverlaps": "无重叠布局", "Noverlaps": "无重叠",
"Force Directed": "力导向布局", "Force Directed": "力导向",
"Force Atlas": "力导向图谱布局" "Force Atlas": "力"
} }
}, },
"fullScreenControl": { "fullScreenControl": {
"fullScreen": "全屏", "fullScreen": "全屏",
"windowed": "窗口模式" "windowed": "窗口"
} }
}, },
"statusIndicator": { "statusIndicator": {
@@ -119,17 +123,17 @@
"storageInfo": "存储信息", "storageInfo": "存储信息",
"workingDirectory": "工作目录", "workingDirectory": "工作目录",
"inputDirectory": "输入目录", "inputDirectory": "输入目录",
"llmConfig": "LLM 配置", "llmConfig": "LLM配置",
"llmBinding": "LLM 绑定", "llmBinding": "LLM绑定",
"llmBindingHost": "LLM 绑定主机", "llmBindingHost": "LLM绑定主机",
"llmModel": "LLM 模型", "llmModel": "LLM模型",
"maxTokens": "最大 Token 数", "maxTokens": "最大令牌数",
"embeddingConfig": "嵌入配置", "embeddingConfig": "嵌入配置",
"embeddingBinding": "嵌入绑定", "embeddingBinding": "嵌入绑定",
"embeddingBindingHost": "嵌入绑定主机", "embeddingBindingHost": "嵌入绑定主机",
"embeddingModel": "嵌入模型", "embeddingModel": "嵌入模型",
"storageConfig": "存储配置", "storageConfig": "存储配置",
"kvStorage": "KV 存储", "kvStorage": "KV存储",
"docStatusStorage": "文档状态存储", "docStatusStorage": "文档状态存储",
"graphStorage": "图存储", "graphStorage": "图存储",
"vectorStorage": "向量存储" "vectorStorage": "向量存储"
@@ -147,90 +151,77 @@
"title": "关系", "title": "关系",
"id": "ID", "id": "ID",
"type": "类型", "type": "类型",
"source": "源", "source": "源节点",
"target": "目标", "target": "目标节点",
"properties": "属性" "properties": "属性"
} }
}, },
"search": { "search": {
"placeholder": "搜索节点...", "placeholder": "搜索节点...",
"message": "以及其它 {count} " "message": "还有 {count} "
}, },
"graphLabels": { "graphLabels": {
"selectTooltip": "选择查询标签", "selectTooltip": "选择查询标签",
"noLabels": "未找到标签", "noLabels": "未找到标签",
"label": "标签", "label": "标签",
"placeholder": "搜索标签...", "placeholder": "搜索标签...",
"andOthers": "以及其它 {count} 个" "andOthers": "还有 {count} 个"
} }
}, },
"retrievePanel": { "retrievePanel": {
"chatMessage": { "chatMessage": {
"copyTooltip": "复制到剪贴板", "copyTooltip": "复制到剪贴板",
"copyError": "无法复制文本到剪贴板" "copyError": "复制文本到剪贴板失败"
}, },
"retrieval": { "retrieval": {
"startPrompt": "在下面输入您的查询开始检索", "startPrompt": "输入查询开始检索",
"clear": "清", "clear": "清",
"send": "发送", "send": "发送",
"placeholder": "输入您的查询...", "placeholder": "输入查询...",
"error": "错误:无法获取响应" "error": "错误:获取响应失败"
}, },
"querySettings": { "querySettings": {
"parametersTitle": "参数设置", "parametersTitle": "参数",
"parametersDescription": "配置查询参数", "parametersDescription": "配置查询参数",
"queryMode": "查询模式", "queryMode": "查询模式",
"queryModeTooltip": "选择检索策略:\n• 朴素:不使用高级技术的基本搜索\n• 本地:基于上下文信息检索\n• 全局:利用全局知识库\n• 混合:结合本地和全局检索\n• 综合:集成知识图谱向量检索", "queryModeTooltip": "选择检索策略:\n• Naive基础搜索无高级技术\n• Local上下文相关信息检索\n• Global:利用全局知识库\n• Hybrid:结合本地和全局检索\n• Mix整合知识图谱向量检索",
"queryModeOptions": { "queryModeOptions": {
"naive": "朴素", "naive": "朴素",
"local": "本地", "local": "本地",
"global": "全局", "global": "全局",
"hybrid": "混合", "hybrid": "混合",
"mix": "合" "mix": "合"
}, },
"responseFormat": "响应格式", "responseFormat": "响应格式",
"responseFormatTooltip": "定义响应格式。例如:\n• 多段落\n• 单段落\n• 项目符号", "responseFormatTooltip": "定义响应格式。例如:\n• 多段落\n• 单段落\n• 要点",
"responseFormatOptions": { "responseFormatOptions": {
"multipleParagraphs": "多段落", "multipleParagraphs": "多段落",
"singleParagraph": "单段落", "singleParagraph": "单段落",
"bulletPoints": "项目符号" "bulletPoints": "要点"
}, },
"topK": "Top K结果",
"topK": "Top K 结果数", "topKTooltip": "检索的顶部项目数。在'local'模式下表示实体,在'global'模式下表示关系",
"topKTooltip": "要检索的前 K 个项目数量。在“本地”模式下表示实体,在“全局”模式下表示关系", "topKPlaceholder": "结果数量",
"topKPlaceholder": "结果数", "maxTokensTextUnit": "文本单元最大令牌数",
"maxTokensTextUnitTooltip": "每个检索文本块允许的最大令牌数",
"maxTokensTextUnit": "文本单元最大 Token 数", "maxTokensGlobalContext": "全局上下文最大令牌数",
"maxTokensTextUnitTooltip": "每个检索到的文本块允许的最大 Token 数", "maxTokensGlobalContextTooltip": "全局检索中关系描述的最大令牌数",
"maxTokensLocalContext": "本地上下文最大令牌数",
"maxTokensGlobalContext": "全局上下文最大 Token 数", "maxTokensLocalContextTooltip": "本地检索中实体描述的最大令牌数",
"maxTokensGlobalContextTooltip": "在全局检索中为关系描述分配的最大 Token 数",
"maxTokensLocalContext": "本地上下文最大 Token 数",
"maxTokensLocalContextTooltip": "在本地检索中为实体描述分配的最大 Token 数",
"historyTurns": "历史轮次", "historyTurns": "历史轮次",
"historyTurnsTooltip": "响应上下文中考虑的完整对话轮次(用户-助手对)", "historyTurnsTooltip": "响应上下文中考虑的完整对话轮次(用户-助手对)数量",
"historyTurnsPlaceholder": "历史轮次的数量", "historyTurnsPlaceholder": "历史轮次",
"hlKeywords": "高级关键词", "hlKeywords": "高级关键词",
"hlKeywordsTooltip": "检索优先考虑的高级关键词。用逗号分隔", "hlKeywordsTooltip": "检索优先考虑的高级关键词列表。用逗号分隔",
"hlkeywordsPlaceHolder": "输入关键词", "hlkeywordsPlaceHolder": "输入关键词",
"llKeywords": "低级关键词", "llKeywords": "低级关键词",
"llKeywordsTooltip": "用于化检索点的低级关键词。用逗号分隔", "llKeywordsTooltip": "用于化检索点的低级关键词列表。用逗号分隔",
"onlyNeedContext": "仅需上下文",
"onlyNeedContext": "仅需要上下文", "onlyNeedContextTooltip": "如果为True仅返回检索到的上下文而不生成响应",
"onlyNeedContextTooltip": "如果为 True则仅返回检索到的上下文而不会生成回复", "onlyNeedPrompt": "仅需提示",
"onlyNeedPromptTooltip": "如果为True仅返回生成的提示而不产生响应",
"onlyNeedPrompt": "仅需要提示",
"onlyNeedPromptTooltip": "如果为 True则仅返回生成的提示而不会生成回复",
"streamResponse": "流式响应", "streamResponse": "流式响应",
"streamResponseTooltip": "如果为 True启用流式输出以获得实时响应" "streamResponseTooltip": "如果为True启用实时流式输出响应"
} }
} }
} }

View File

@@ -6,6 +6,7 @@ import { Message, QueryRequest } from '@/api/lightrag'
import { useGraphStore } from '@/stores/graph' import { useGraphStore } from '@/stores/graph'
type Theme = 'dark' | 'light' | 'system' type Theme = 'dark' | 'light' | 'system'
type Language = 'en' | 'zh'
type Tab = 'documents' | 'knowledge-graph' | 'retrieval' | 'api' type Tab = 'documents' | 'knowledge-graph' | 'retrieval' | 'api'
interface SettingsState { interface SettingsState {
@@ -48,6 +49,9 @@ interface SettingsState {
theme: Theme theme: Theme
setTheme: (theme: Theme) => void setTheme: (theme: Theme) => void
language: Language
setLanguage: (lang: Language) => void
enableHealthCheck: boolean enableHealthCheck: boolean
setEnableHealthCheck: (enable: boolean) => void setEnableHealthCheck: (enable: boolean) => void
@@ -59,6 +63,7 @@ const useSettingsStoreBase = create<SettingsState>()(
persist( persist(
(set) => ({ (set) => ({
theme: 'system', theme: 'system',
language: 'zh',
refreshLayout: () => { refreshLayout: () => {
const graphState = useGraphStore.getState(); const graphState = useGraphStore.getState();
const currentGraph = graphState.sigmaGraph; const currentGraph = graphState.sigmaGraph;
@@ -110,6 +115,13 @@ const useSettingsStoreBase = create<SettingsState>()(
setTheme: (theme: Theme) => set({ theme }), setTheme: (theme: Theme) => set({ theme }),
setLanguage: (language: Language) => {
import('i18next').then(({ default: i18n }) => {
i18n.changeLanguage(language);
});
set({ language });
},
setGraphLayoutMaxIterations: (iterations: number) => setGraphLayoutMaxIterations: (iterations: number) =>
set({ set({
graphLayoutMaxIterations: iterations graphLayoutMaxIterations: iterations