mirror of
https://github.com/xxnuo/MTranServer.git
synced 2026-09-03 06:35:20 +08:00
feat: update frontend
This commit is contained in:
9
Makefile
9
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: download download-core download-records generate-docs
|
||||
.PHONY: download download-core download-records generate-docs build-ui
|
||||
|
||||
# Detect OS and architecture
|
||||
GOOS ?= $(shell go env GOOS)
|
||||
@@ -39,11 +39,16 @@ generate-docs:
|
||||
@GOOS= GOARCH= go run github.com/swaggo/swag/cmd/swag@latest init -g ./cmd/mtranserver/main.go -o ./internal/docs
|
||||
@echo "Docs generated successfully"
|
||||
|
||||
build-ui:
|
||||
@echo "Building UI..."
|
||||
@cd ui && pnpm install && pnpm build
|
||||
@echo "UI built successfully"
|
||||
|
||||
# Get version from git tag
|
||||
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "v0.0.0-dev")
|
||||
LDFLAGS := -X github.com/xxnuo/MTranServer/internal/version.Version=$(VERSION)
|
||||
|
||||
build: generate-docs
|
||||
build: generate-docs build-ui
|
||||
@echo "Building version $(VERSION)..."
|
||||
@go build -ldflags "$(LDFLAGS)" -o ./dist/mtranserver-$(GOOS)-$(GOARCH)$(SUFFIX) ./cmd/mtranserver
|
||||
@echo "Built successfully"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
@@ -8,6 +10,7 @@ import (
|
||||
"github.com/xxnuo/MTranServer/internal/docs"
|
||||
"github.com/xxnuo/MTranServer/internal/handlers"
|
||||
"github.com/xxnuo/MTranServer/internal/middleware"
|
||||
"github.com/xxnuo/MTranServer/ui"
|
||||
)
|
||||
|
||||
// Setup 设置所有路由
|
||||
@@ -43,4 +46,14 @@ func Setup(r *gin.Engine, apiToken string) {
|
||||
r.POST("/google/language/translate/v2", handlers.HandleGoogleCompatTranslate(apiToken))
|
||||
r.GET("/google/translate_a/single", handlers.HandleGoogleTranslateSingle(apiToken))
|
||||
r.POST("/hcfy", handlers.HandleHcfyTranslate(apiToken))
|
||||
|
||||
// 前端静态文件服务
|
||||
distFS, err := ui.GetDistFS()
|
||||
if err == nil {
|
||||
r.StaticFS("/ui", http.FS(distFS))
|
||||
// 根路径重定向到 /ui
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.Redirect(http.StatusMovedPermanently, "/ui/")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
73
ui/README.md
73
ui/README.md
@@ -1,73 +0,0 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is currently not compatible with SWC. See [this issue](https://github.com/vitejs/vite-plugin-react/issues/428) for tracking the progress.
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
@@ -2,9 +2,10 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/png" href="/icon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ui</title>
|
||||
<meta name="darkreader-lock" />
|
||||
<title>MTranServer</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"i18next": "^25.6.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.553.0",
|
||||
"next-themes": "^0.4.6",
|
||||
@@ -50,6 +52,7 @@
|
||||
"react-day-picker": "^9.11.1",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-hook-form": "^7.66.0",
|
||||
"react-i18next": "^16.3.3",
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"recharts": "2.15.4",
|
||||
"sonner": "^2.0.7",
|
||||
|
||||
265
ui/src/App.tsx
265
ui/src/App.tsx
@@ -1,34 +1,247 @@
|
||||
import { useState } from 'react'
|
||||
import reactLogo from './assets/react.svg'
|
||||
import viteLogo from '/vite.svg'
|
||||
import './App.css'
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Spinner } from '@/components/ui/spinner'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { toast } from 'sonner'
|
||||
import { ArrowRightLeft } from 'lucide-react'
|
||||
import { SettingsMenu } from '@/components/SettingsMenu'
|
||||
|
||||
interface TranslateRequest {
|
||||
from: string
|
||||
to: string
|
||||
text: string
|
||||
html: boolean
|
||||
}
|
||||
|
||||
interface TranslateResponse {
|
||||
result: string
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
const { t } = useTranslation()
|
||||
const [languages, setLanguages] = useState<string[]>([])
|
||||
const [sourceLang, setSourceLang] = useState('auto')
|
||||
const [targetLang, setTargetLang] = useState('zh-Hans')
|
||||
const [sourceText, setSourceText] = useState('')
|
||||
const [translatedText, setTranslatedText] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [loadingLanguages, setLoadingLanguages] = useState(true)
|
||||
const [autoTranslate, setAutoTranslate] = useState(false)
|
||||
const translateTimeoutRef = useRef<number | null>(null)
|
||||
|
||||
const fetchLanguages = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch('/languages')
|
||||
if (!response.ok) throw new Error('Failed to fetch languages')
|
||||
const data = await response.json()
|
||||
setLanguages(['auto', ...(data.languages || [])])
|
||||
} catch (error) {
|
||||
console.error('Error fetching languages:', error)
|
||||
toast.error(t('failedToLoadLanguages'))
|
||||
} finally {
|
||||
setLoadingLanguages(false)
|
||||
}
|
||||
}, [t])
|
||||
|
||||
useEffect(() => {
|
||||
fetchLanguages()
|
||||
}, [fetchLanguages])
|
||||
|
||||
const handleTranslate = useCallback(async (text?: string, showToast = true) => {
|
||||
const textToTranslate = text ?? sourceText
|
||||
if (!textToTranslate.trim()) {
|
||||
if (showToast) {
|
||||
toast.error(t('enterTextError'))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setTranslatedText('')
|
||||
|
||||
try {
|
||||
const request: TranslateRequest = {
|
||||
from: sourceLang,
|
||||
to: targetLang,
|
||||
text: textToTranslate,
|
||||
html: false
|
||||
}
|
||||
|
||||
const response = await fetch('/translate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(request)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || t('translationFailed'))
|
||||
}
|
||||
|
||||
const data: TranslateResponse = await response.json()
|
||||
setTranslatedText(data.result)
|
||||
if (showToast) {
|
||||
toast.success(t('translationCompleted'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Translation error:', error)
|
||||
if (showToast) {
|
||||
toast.error(error instanceof Error ? error.message : t('translationFailed'))
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [sourceLang, targetLang, sourceText, t])
|
||||
|
||||
const handleSourceTextChange = (text: string) => {
|
||||
setSourceText(text)
|
||||
|
||||
if (autoTranslate && text.trim()) {
|
||||
if (translateTimeoutRef.current) {
|
||||
clearTimeout(translateTimeoutRef.current)
|
||||
}
|
||||
|
||||
translateTimeoutRef.current = window.setTimeout(() => {
|
||||
handleTranslate(text, false)
|
||||
}, 800)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (translateTimeoutRef.current) {
|
||||
window.clearTimeout(translateTimeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleSwapLanguages = () => {
|
||||
setSourceLang(targetLang)
|
||||
setTargetLang(sourceLang)
|
||||
setSourceText(translatedText)
|
||||
setTranslatedText(sourceText)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://react.dev" target="_blank">
|
||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||
</a>
|
||||
<div className="min-h-screen bg-background p-4 md:p-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
{t('title')}
|
||||
</h1>
|
||||
<SettingsMenu />
|
||||
</div>
|
||||
|
||||
<Card className="shadow-lg">
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
<div className="flex items-center gap-2 justify-between">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<Select
|
||||
value={sourceLang}
|
||||
onValueChange={setSourceLang}
|
||||
disabled={loadingLanguages}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{languages.map((lang) => (
|
||||
<SelectItem key={lang} value={lang}>
|
||||
{lang === 'auto' ? t('autoDetect') : lang}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleSwapLanguages}
|
||||
disabled={loadingLanguages || sourceLang === 'auto'}
|
||||
className="h-9 w-9"
|
||||
>
|
||||
<ArrowRightLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Select
|
||||
value={targetLang}
|
||||
onValueChange={setTargetLang}
|
||||
disabled={loadingLanguages}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{languages.filter(lang => lang !== 'auto').map((lang) => (
|
||||
<SelectItem key={lang} value={lang}>
|
||||
{lang}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="auto-translate"
|
||||
checked={autoTranslate}
|
||||
onCheckedChange={setAutoTranslate}
|
||||
/>
|
||||
<Label htmlFor="auto-translate" className="text-xs cursor-pointer whitespace-nowrap">
|
||||
{t('autoTranslate')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Textarea
|
||||
id="source-text"
|
||||
placeholder={t('enterText')}
|
||||
value={sourceText}
|
||||
onChange={(e) => handleSourceTextChange(e.target.value)}
|
||||
className="min-h-[300px] resize-none text-base"
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
id="translated-text"
|
||||
placeholder={t('translationWillAppear')}
|
||||
value={translatedText}
|
||||
readOnly
|
||||
className="min-h-[300px] resize-none text-base bg-muted"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!autoTranslate && (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
onClick={() => handleTranslate()}
|
||||
disabled={loading || loadingLanguages || !sourceText.trim()}
|
||||
className="min-w-[200px]"
|
||||
size="lg"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Spinner className="mr-2 h-4 w-4" />
|
||||
{t('translating')}
|
||||
</>
|
||||
) : (
|
||||
t('translate')
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<h1>Vite + React</h1>
|
||||
<div className="card">
|
||||
<button onClick={() => setCount((count) => count + 1)}>
|
||||
count is {count}
|
||||
</button>
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to test HMR
|
||||
</p>
|
||||
</div>
|
||||
<p className="read-the-docs">
|
||||
Click on the Vite and React logos to learn more
|
||||
</p>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
56
ui/src/components/SettingsMenu.tsx
Normal file
56
ui/src/components/SettingsMenu.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useTheme } from '@/contexts/ThemeContext'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Sun, Moon, Globe } from 'lucide-react'
|
||||
|
||||
export function SettingsMenu() {
|
||||
const { i18n } = useTranslation()
|
||||
const { actualTheme, setTheme } = useTheme()
|
||||
|
||||
const languages = [
|
||||
{ code: 'en', name: 'EN' },
|
||||
{ code: 'zh', name: '中' },
|
||||
{ code: 'ja', name: '日' }
|
||||
]
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(actualTheme === 'dark' ? 'light' : 'dark')
|
||||
}
|
||||
|
||||
const cycleLanguage = () => {
|
||||
const currentIndex = languages.findIndex(lang => lang.code === i18n.language)
|
||||
const nextIndex = (currentIndex + 1) % languages.length
|
||||
i18n.changeLanguage(languages[nextIndex].code)
|
||||
}
|
||||
|
||||
const currentLang = languages.find(lang => lang.code === i18n.language) || languages[0]
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={cycleLanguage}
|
||||
className="h-9 w-9"
|
||||
title="Switch Language"
|
||||
>
|
||||
<Globe className="h-4 w-4" />
|
||||
<span className="sr-only">{currentLang.name}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleTheme}
|
||||
className="h-9 w-9"
|
||||
title="Toggle Theme"
|
||||
>
|
||||
{actualTheme === 'dark' ? (
|
||||
<Sun className="h-4 w-4" />
|
||||
) : (
|
||||
<Moon className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
61
ui/src/contexts/ThemeContext.tsx
Normal file
61
ui/src/contexts/ThemeContext.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
|
||||
type Theme = 'light' | 'dark' | 'system'
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme
|
||||
setTheme: (theme: Theme) => void
|
||||
actualTheme: 'light' | 'dark'
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
const stored = localStorage.getItem('theme')
|
||||
return (stored as Theme) || 'system'
|
||||
})
|
||||
|
||||
const [actualTheme, setActualTheme] = useState<'light' | 'dark'>('light')
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement
|
||||
|
||||
const getSystemTheme = () => {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
const applyTheme = (t: Theme) => {
|
||||
const themeToApply = t === 'system' ? getSystemTheme() : t
|
||||
setActualTheme(themeToApply)
|
||||
|
||||
root.classList.remove('light', 'dark')
|
||||
root.classList.add(themeToApply)
|
||||
}
|
||||
|
||||
applyTheme(theme)
|
||||
localStorage.setItem('theme', theme)
|
||||
|
||||
if (theme === 'system') {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const handler = () => applyTheme('system')
|
||||
mediaQuery.addEventListener('change', handler)
|
||||
return () => mediaQuery.removeEventListener('change', handler)
|
||||
}
|
||||
}, [theme])
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme, actualTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext)
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within ThemeProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
105
ui/src/i18n/index.ts
Normal file
105
ui/src/i18n/index.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import i18n from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
translation: {
|
||||
title: 'MTranServer',
|
||||
subtitle: 'Fast and reliable translation service',
|
||||
translate: 'Translate',
|
||||
translating: 'Translating...',
|
||||
sourceLanguage: 'Source Language',
|
||||
targetLanguage: 'Target Language',
|
||||
sourceText: 'Source Text',
|
||||
translation: 'Translation',
|
||||
enterText: 'Enter text to translate...',
|
||||
translationWillAppear: 'Translation will appear here...',
|
||||
selectLanguage: 'Select language',
|
||||
autoDetect: 'Auto Detect',
|
||||
translationCompleted: 'Translation completed',
|
||||
translationFailed: 'Translation failed',
|
||||
enterTextError: 'Please enter text to translate',
|
||||
failedToLoadLanguages: 'Failed to load languages',
|
||||
theme: 'Theme',
|
||||
language: 'Language',
|
||||
light: 'Light',
|
||||
dark: 'Dark',
|
||||
system: 'System',
|
||||
autoTranslate: 'Auto Translate',
|
||||
autoTranslateDesc: 'Automatically translate as you type'
|
||||
}
|
||||
},
|
||||
zh: {
|
||||
translation: {
|
||||
title: 'MTranServer',
|
||||
subtitle: '快速可靠的翻译服务',
|
||||
translate: '翻译',
|
||||
translating: '翻译中...',
|
||||
sourceLanguage: '源语言',
|
||||
targetLanguage: '目标语言',
|
||||
sourceText: '源文本',
|
||||
translation: '翻译结果',
|
||||
enterText: '输入要翻译的文本...',
|
||||
translationWillAppear: '翻译结果将显示在这里...',
|
||||
selectLanguage: '选择语言',
|
||||
autoDetect: '自动检测',
|
||||
translationCompleted: '翻译完成',
|
||||
translationFailed: '翻译失败',
|
||||
enterTextError: '请输入要翻译的文本',
|
||||
failedToLoadLanguages: '加载语言列表失败',
|
||||
theme: '主题',
|
||||
language: '语言',
|
||||
light: '浅色',
|
||||
dark: '深色',
|
||||
system: '跟随系统',
|
||||
autoTranslate: '自动翻译',
|
||||
autoTranslateDesc: '输入时自动翻译'
|
||||
}
|
||||
},
|
||||
ja: {
|
||||
translation: {
|
||||
title: 'MTranServer',
|
||||
subtitle: '高速で信頼性の高い翻訳サービス',
|
||||
translate: '翻訳',
|
||||
translating: '翻訳中...',
|
||||
sourceLanguage: 'ソース言語',
|
||||
targetLanguage: 'ターゲット言語',
|
||||
sourceText: 'ソーステキスト',
|
||||
translation: '翻訳',
|
||||
enterText: '翻訳するテキストを入力...',
|
||||
translationWillAppear: '翻訳がここに表示されます...',
|
||||
selectLanguage: '言語を選択',
|
||||
autoDetect: '自動検出',
|
||||
translationCompleted: '翻訳完了',
|
||||
translationFailed: '翻訳失敗',
|
||||
enterTextError: '翻訳するテキストを入力してください',
|
||||
failedToLoadLanguages: '言語の読み込みに失敗しました',
|
||||
theme: 'テーマ',
|
||||
language: '言語',
|
||||
light: 'ライト',
|
||||
dark: 'ダーク',
|
||||
system: 'システム',
|
||||
autoTranslate: '自動翻訳',
|
||||
autoTranslateDesc: '入力時に自動翻訳'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: 'en',
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
},
|
||||
detection: {
|
||||
order: ['localStorage', 'navigator'],
|
||||
caches: ['localStorage']
|
||||
}
|
||||
})
|
||||
|
||||
export default i18n
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import './i18n'
|
||||
import App from './App.tsx'
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import { ThemeProvider } from './contexts/ThemeContext'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
14
ui/ui.go
Normal file
14
ui/ui.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
//go:embed all:dist
|
||||
var distFS embed.FS
|
||||
|
||||
// GetDistFS returns the embedded dist filesystem
|
||||
func GetDistFS() (fs.FS, error) {
|
||||
return fs.Sub(distFS, "dist")
|
||||
}
|
||||
@@ -5,10 +5,15 @@ import tailwindcss from "@tailwindcss/vite"
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
base: '/ui/',
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user