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'
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(theme)
root.classList.add(mediaQuery.matches ? 'dark' : 'light')
mediaQuery.addEventListener('change', handleChange)
return () => mediaQuery.removeEventListener('change', handleChange)
} else {
root.classList.add(theme)
}
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
useSettingsStore.getState().setTheme(theme)
setTheme(theme)
}
setTheme
}
return (

View File

@@ -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>
)

View File

@@ -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
}

View File

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

View File

@@ -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启用实时流式输出响应"
}
}
}

View File

@@ -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