Skip to content

Commit

Permalink
feat: 增加 i18n 语言支持
Browse files Browse the repository at this point in the history
chore: 优化 store 和 i18n 初始化逻辑
  • Loading branch information
Amery2010 committed Jan 3, 2024
1 parent aebb106 commit e272842
Show file tree
Hide file tree
Showing 28 changed files with 553 additions and 123 deletions.
8 changes: 5 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{
"editor.formatOnSave": true,
"css.lint.unknownAtRules": "ignore"
}
"editor.formatOnSave": true,
"css.lint.unknownAtRules": "ignore",
"i18n-ally.localesPaths": ["locales"],
"i18n-ally.keystyle": "nested"
}
7 changes: 6 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { Metadata } from 'next'
import ThemeProvider from '@/components/ThemeProvider'
import StoreProvider from '@/components/StoreProvider'
import I18Provider from '@/components/I18nProvider'

import 'katex/dist/katex.min.css'
import 'highlight.js/styles/a11y-light.css'
import './globals.css'
Expand All @@ -14,7 +17,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{children}
<StoreProvider>
<I18Provider>{children}</I18Provider>
</StoreProvider>
</ThemeProvider>
</body>
</html>
Expand Down
33 changes: 15 additions & 18 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
'use client'
import { useRef, useState, useMemo, useLayoutEffect, KeyboardEvent, useEffect } from 'react'
import { useRef, useState, useMemo, KeyboardEvent } from 'react'
import { EdgeSpeechTTS } from '@lobehub/tts'
import { useSpeechRecognition } from '@lobehub/tts/react'
import { useAutoAnimate } from '@formkit/auto-animate/react'
import SiriWave from 'siriwave'
import { MessageCircleHeart, AudioLines, Mic, MessageSquareText, Settings, Pause } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import ThemeToggle from '@/components/ThemeToggle'
import { Textarea } from '@/components/ui/textarea'
import { Button } from '@/components/ui/button'
Expand All @@ -22,6 +23,7 @@ import textStream from '@/utils/textStream'
import { generateSignature, generateUTCTimestamp } from '@/utils/signature'

export default function Home() {
const { t } = useTranslation()
const siriWaveRef = useRef<HTMLDivElement>(null)
const audioStreamRef = useRef<AudioStream>()
const edgeSpeechRef = useRef<EdgeSpeechTTS>()
Expand Down Expand Up @@ -53,9 +55,9 @@ export default function Home() {
return ''
case 'thinkng':
default:
return '正在思考'
return t('status.thinking')
}
}, [status])
}, [status, t])

const speech = (content: string) => {
if (content.length === 0) return
Expand Down Expand Up @@ -262,18 +264,13 @@ export default function Home() {
}
}

useLayoutEffect(() => {
initMessages()
initSetting()
}, [initMessages, initSetting])

return (
<main className="mx-auto flex min-h-screen max-w-screen-md flex-col justify-between pt-6 max-sm:pt-0">
<div className="mb-2 mt-6 flex justify-between p-4 max-sm:mt-2">
<div className="flex flex-row text-xl leading-8">
<MessageCircleHeart className="h-10 w-10 text-red-400" />
<div className="ml-3 bg-gradient-to-r from-red-300 via-green-300 to-green-400 bg-clip-text font-bold leading-10 text-transparent">
Talk With Gemini Pro
{t('title')}
</div>
</div>
<ThemeToggle />
Expand All @@ -294,30 +291,30 @@ export default function Home() {
{msg.role === 'model' && idx === messages.length - 3 ? (
<div className="my-2 flex h-4 justify-center text-xs text-slate-400 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:text-slate-600">
<span className="mx-2 cursor-pointer hover:text-slate-500" onClick={() => handleResubmit()}>
重新生成答案
{t('regenerateAnswer')}
</span>
<Separator orientation="vertical" />
<span className="mx-2 cursor-pointer hover:text-slate-500" onClick={() => handleCleanMessage()}>
清空聊天内容
{t('clearChatContent')}
</span>
</div>
) : null}
</div>
))}
</div>
<div className="flex w-full max-w-screen-md gap-2 bg-[hsl(var(--background))] p-4 pb-8 max-sm:pb-4">
<Button title="语音对话模式" variant="secondary" size="icon" onClick={() => updateTalkMode('voice')}>
<Button title={t('voiceMode')} variant="secondary" size="icon" onClick={() => updateTalkMode('voice')}>
<AudioLines />
</Button>
<Textarea
className="min-h-10"
rows={1}
value={content}
placeholder="请输入问题..."
placeholder={t('askAQuestion')}
onChange={(ev) => setContent(ev.target.value)}
onKeyDown={handleKeyDown}
/>
<Button title="设置" variant="secondary" size="icon" onClick={() => setSetingOpen(true)}>
<Button title={t('setting')} variant="secondary" size="icon" onClick={() => setSetingOpen(true)}>
<Settings />
</Button>
</div>
Expand All @@ -336,7 +333,7 @@ export default function Home() {
<div className="flex items-center justify-center pt-2">
<Button
className="h-10 w-10 rounded-full text-slate-700"
title="聊天模式"
title={t('chatMode')}
variant="secondary"
size="icon"
onClick={() => updateTalkMode('chat')}
Expand All @@ -346,7 +343,7 @@ export default function Home() {
{status === 'talking' ? (
<Button
className="mx-6 h-14 w-14 rounded-full"
title="停止说话"
title={t('stopTalking')}
variant="destructive"
size="icon"
onClick={() => handleStopTalking()}
Expand All @@ -356,7 +353,7 @@ export default function Home() {
) : (
<Button
className="mx-6 h-14 w-14 rounded-full font-mono"
title="开始录音"
title={t('startRecording')}
variant="destructive"
size="icon"
onClick={() => handleRecorder()}
Expand All @@ -366,7 +363,7 @@ export default function Home() {
)}
<Button
className="h-10 w-10 rounded-full text-slate-700"
title="设置"
title={t('setting')}
variant="secondary"
size="icon"
onClick={() => setSetingOpen(true)}
Expand Down
16 changes: 16 additions & 0 deletions components/I18nProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use client'
import { useLayoutEffect } from 'react'
import { I18nextProvider } from 'react-i18next'
import { useSettingStore } from '@/store/setting'
import i18n from '@/plugins/i18n'

function I18Provider({ children }: { children: React.ReactNode }) {
const { lang } = useSettingStore()

useLayoutEffect(() => {
i18n.changeLanguage(lang)
}, [lang])
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>
}

export default I18Provider
7 changes: 0 additions & 7 deletions components/MessageItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,6 @@ const registerCopy = (className: string) => {
return decodeURIComponent(trigger.getAttribute('data-clipboard-text') || '')
},
})
// 复制成功失败的提示
clipboard.on('success', () => {
console.info('复制成功')
})
clipboard.on('error', () => {
console.error('复制失败')
})
return clipboard
}

Expand Down
94 changes: 58 additions & 36 deletions components/Setting.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { memo, useEffect, useMemo, useState } from 'react'
import { EdgeSpeechTTS } from '@lobehub/tts'
import { useSettingStore } from '@/store/setting'
import { useTranslation } from 'react-i18next'
import {
Dialog,
DialogContent,
Expand All @@ -13,7 +13,9 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'
import i18n from '@/plugins/i18n'
import locales from '@/constant/locales'
import { useSettingStore } from '@/store/setting'
import { toPairs, values } from 'lodash-es'

type SettingProps = {
Expand All @@ -22,10 +24,12 @@ type SettingProps = {
}

function Setting({ open, onClose }: SettingProps) {
const { t } = useTranslation()
const settingStore = useSettingStore()
const [password, setPassword] = useState<string>('')
const [apiKey, setApiKey] = useState<string>('')
const [apiProxy, setApiProxy] = useState<string>('')
const [lang, setLang] = useState<string>('')
const [sttLang, setSttLang] = useState<string>('')
const [ttsLang, setTtsLang] = useState<string>('')
const [ttsVoice, setTtsVoice] = useState<string>('')
Expand All @@ -37,6 +41,7 @@ function Setting({ open, onClose }: SettingProps) {
if (password !== settingStore.password) settingStore.setPassword(password)
if (apiKey !== settingStore.apiKey) settingStore.setApiKey(apiKey)
if (apiProxy !== settingStore.apiProxy) settingStore.setApiProxy(apiProxy)
if (lang !== settingStore.lang) settingStore.setLang(lang)
if (sttLang !== settingStore.sttLang) settingStore.setSTTLang(sttLang)
if (ttsLang !== settingStore.ttsLang) settingStore.setTTSLang(ttsLang)
if (ttsVoice !== settingStore.ttsVoice) settingStore.setTTSVoice(ttsVoice)
Expand All @@ -54,10 +59,29 @@ function Setting({ open, onClose }: SettingProps) {
}
}

const handleLangChange = (value: string) => {
i18n.changeLanguage(value)
setLang(value)
setSttLang(value)
setTtsLang(value)
handleTTSChange(value)
}

const LangOptions = () => {
return toPairs(locales).map((kv) => {
return (
<SelectItem key={kv[0]} value={kv[0]}>
{kv[1]}
</SelectItem>
)
})
}

useEffect(() => {
setPassword(settingStore.password)
setApiKey(settingStore.apiKey)
setApiProxy(settingStore.apiProxy)
setLang(settingStore.lang)
setSttLang(settingStore.sttLang)
setTtsLang(settingStore.ttsLang)
setTtsVoice(settingStore.ttsVoice)
Expand All @@ -67,24 +91,19 @@ function Setting({ open, onClose }: SettingProps) {
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-sm:h-full">
<DialogHeader>
<DialogTitle>设置</DialogTitle>
<DialogDescription>
请输入访问密码或者使用自己的{' '}
<a className="underline underline-offset-4" href="https://ai.google.dev/" target="_blank">
Gemini 密钥
</a>
,密钥通过浏览器发送请求,不会转发到后端服务器。
</DialogDescription>
<DialogTitle>{t('setting')}</DialogTitle>
<DialogDescription>{t('settingDescription')}</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="password" className="text-right">
<span className="leading-12 mr-1 text-red-500">*</span>访问密码
<span className="leading-12 mr-1 text-red-500">*</span>
{t('accessPassword')}
</Label>
<Input
id="password"
type="password"
placeholder="请输入访问密码"
placeholder={t('accessPasswordPlaceholder')}
className="col-span-3"
defaultValue={password}
onChange={(ev) => setPassword(ev.target.value)}
Expand All @@ -93,24 +112,25 @@ function Setting({ open, onClose }: SettingProps) {
<hr />
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="key" className="text-right">
<span className="leading-12 mr-1 text-red-500">*</span>密钥
<span className="leading-12 mr-1 text-red-500">*</span>
{t('geminiKey')}
</Label>
<Input
id="key"
type="password"
placeholder="请输入 Gemini 密钥"
placeholder={t('geminiKeyPlaceholder')}
className="col-span-3"
defaultValue={apiKey}
onChange={(ev) => setApiKey(ev.target.value)}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="proxy" className="text-right">
接口地址
{t('apiProxyUrl')}
</Label>
<Input
id="proxy"
placeholder="请输入接口代理地址(可选)"
placeholder={t('apiProxyUrlPlaceholder')}
className="col-span-3"
defaultValue={apiProxy}
onChange={(ev) => setApiProxy(ev.target.value)}
Expand All @@ -119,49 +139,37 @@ function Setting({ open, onClose }: SettingProps) {
<hr />
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="stt" className="text-right">
语音识别
{t('speechRecognition')}
</Label>
<Select value={sttLang} onValueChange={setSttLang}>
<SelectTrigger id="stt" className="col-span-3">
<SelectValue placeholder="跟随系统" />
<SelectValue placeholder={t('followTheSystem')} />
</SelectTrigger>
<SelectContent>
{toPairs(locales).map((kv) => {
return (
<SelectItem key={kv[0]} value={kv[0]}>
{kv[1]}
</SelectItem>
)
})}
<LangOptions />
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="tts" className="text-right">
语音合成
{t('speechSynthesis')}
</Label>
<Select value={ttsLang} onValueChange={handleTTSChange}>
<SelectTrigger id="tts" className="col-span-3">
<SelectValue placeholder="跟随系统" />
<SelectValue placeholder={t('followTheSystem')} />
</SelectTrigger>
<SelectContent>
{toPairs(locales).map((kv) => {
return (
<SelectItem key={kv[0]} value={kv[0]}>
{kv[1]}
</SelectItem>
)
})}
<LangOptions />
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="tts" className="text-right">
合成声源
{t('soundSource')}
</Label>
<Select value={ttsVoice} onValueChange={setTtsVoice}>
<SelectTrigger id="tts" className="col-span-3">
<SelectValue placeholder="跟随系统" />
<SelectValue placeholder={t('followTheSystem')} />
</SelectTrigger>
<SelectContent>
{values(voiceOptions).map((option) => {
Expand All @@ -174,10 +182,24 @@ function Setting({ open, onClose }: SettingProps) {
</SelectContent>
</Select>
</div>
<hr />
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="stt" className="text-right">
{t('language')}
</Label>
<Select value={lang} onValueChange={handleLangChange}>
<SelectTrigger id="stt" className="col-span-3">
<SelectValue placeholder={t('followTheSystem')} />
</SelectTrigger>
<SelectContent>
<LangOptions />
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="submit" onClick={handleSubmit}>
保存
{t('save')}
</Button>
</DialogFooter>
</DialogContent>
Expand Down
Loading

0 comments on commit e272842

Please sign in to comment.