├── . dockerignore ├── .eslintrc.json ├── app ├── icon.png ├── fonts │ ├── GeistVF.woff │ └── GeistMonoVF.woff ├── adapter │ ├── yiyan │ │ ├── logo.png │ │ ├── login.ts │ │ ├── models.ts │ │ ├── utils.ts │ │ ├── api.ts │ │ ├── translater.tsx │ │ ├── settings.tsx │ │ └── yiyan.svg │ ├── moonshot │ │ ├── logo.png │ │ ├── models.ts │ │ ├── utils.ts │ │ ├── translater.tsx │ │ ├── settings.tsx │ │ └── api.ts │ ├── claude │ │ ├── models.tsx │ │ ├── claude_logo.svg │ │ ├── utils.ts │ │ ├── claude_text.svg │ │ ├── api.ts │ │ ├── translater.tsx │ │ ├── anthropic.svg │ │ └── settings.tsx │ ├── openai │ │ ├── models.ts │ │ ├── utils.ts │ │ ├── logo.svg │ │ ├── api.ts │ │ ├── translater.tsx │ │ └── settings.tsx │ └── interface.ts ├── images │ ├── providers │ │ ├── claude.png │ │ ├── google.png │ │ ├── openai.png │ │ ├── moonshot.png │ │ └── qianfan.jpg │ └── logo.svg ├── unils.ts ├── globals.css ├── settings │ └── providers │ │ └── page.tsx ├── layout.tsx └── page.tsx ├── postcss.config.mjs ├── next.config.mjs ├── tailwind.config.ts ├── Dockerfile ├── .gitignore ├── tsconfig.json ├── README.md ├── locales ├── zh.json ├── ja.json └── en.json ├── package.json ├── i18n └── request.ts └── components └── header.tsx /. dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .env -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/icon.png -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/adapter/yiyan/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/adapter/yiyan/logo.png -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /app/adapter/moonshot/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/adapter/moonshot/logo.png -------------------------------------------------------------------------------- /app/images/providers/claude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/images/providers/claude.png -------------------------------------------------------------------------------- /app/images/providers/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/images/providers/google.png -------------------------------------------------------------------------------- /app/images/providers/openai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/images/providers/openai.png -------------------------------------------------------------------------------- /app/images/providers/moonshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/images/providers/moonshot.png -------------------------------------------------------------------------------- /app/images/providers/qianfan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/images/providers/qianfan.jpg -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import createNextIntlPlugin from 'next-intl/plugin'; 2 | 3 | const withNextIntl = createNextIntlPlugin(); 4 | /** @type {import('next').NextConfig} */ 5 | const nextConfig = {}; 6 | 7 | export default withNextIntl(nextConfig); -------------------------------------------------------------------------------- /app/unils.ts: -------------------------------------------------------------------------------- 1 | export function addIfNotExists(arr: string[], element: string) { 2 | if (!arr.includes(element)) { 3 | arr.push(element); 4 | } 5 | return arr; 6 | } 7 | export function removeIfExists(arr: string[], element: string) { 8 | const index = arr.indexOf(element); 9 | if (index !== -1) { 10 | arr.splice(index, 1); 11 | } 12 | return arr; 13 | } -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | background: "var(--background)", 13 | foreground: "var(--foreground)", 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | }; 19 | export default config; 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 使用 Node.js 官方镜像作为基础镜像 2 | FROM node:20-slim AS base 3 | 4 | # 设置工作目录 5 | WORKDIR /app 6 | 7 | # 复制 package.json 和 package-lock.json 8 | COPY package*.json ./ 9 | 10 | # 安装依赖 11 | RUN npm config set registry https://registry.npmmirror.com 12 | RUN npm install 13 | 14 | # 复制所有项目文件 15 | COPY . . 16 | 17 | # 构建 Next.js 应用 18 | RUN npm run build 19 | 20 | # 设置环境变量 21 | ENV NODE_ENV production 22 | 23 | # 暴露应用运行的端口 24 | EXPOSE 3000 25 | 26 | # 启动 Next.js 应用 27 | CMD ["npm", "start"] 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /app/adapter/moonshot/models.ts: -------------------------------------------------------------------------------- 1 | import { LLMModel } from "@/app/adapter/interface" 2 | export const provider = { 3 | id: 'moonshot', 4 | providerName: 'Moonshot AI', 5 | } 6 | 7 | export const modelList: LLMModel[] = [ 8 | { 9 | 'name': 'moonshot-v1-8k', 10 | 'displayName': 'Moonshot v1 8K', 11 | provider 12 | }, 13 | { 14 | 'name': 'moonshot-v1-32k', 15 | 'displayName': 'Moonshot v1 32K', 16 | provider 17 | }, 18 | { 19 | 'name': 'moonshot-v1-128k', 20 | 'displayName': 'Moonshot v1 128K', 21 | provider 22 | }, 23 | ] -------------------------------------------------------------------------------- /app/adapter/yiyan/login.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | export async function login(apikey: string, secretkey: string) { 3 | const Loginurl = `https://aip.baidubce.com/oauth/2.0/token?client_id=${apikey}&client_secret=${secretkey}&grant_type=client_credentials`; 4 | const resp = await fetch(Loginurl).catch(() => { 5 | return new Response('{"error": true, "message": "input error"}', { status: 504, statusText: "input error" }) 6 | } 7 | ); 8 | const result = await resp.json(); 9 | if (result.error) { 10 | return { error: true, message: `校验失败,API Key 或 Secret Key 错误` }; 11 | } else { 12 | return result; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: Arial, Helvetica, sans-serif; 21 | } 22 | 23 | @layer utilities { 24 | .text-balance { 25 | text-wrap: balance; 26 | } 27 | } 28 | 29 | .translate-result p{ 30 | margin-bottom: 1em; 31 | } 32 | 33 | .translate-result :last-child{ 34 | margin-bottom: 0; 35 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /app/adapter/claude/models.tsx: -------------------------------------------------------------------------------- 1 | import { LLMModel } from "@/app/adapter/interface" 2 | export const provider = { 3 | id: 'claude', 4 | providerName: 'Claude AI', 5 | } 6 | 7 | export const modelList: LLMModel[] = [ 8 | { 9 | 'name': 'claude-3-5-sonnet-20240620', 10 | 'displayName': 'Claude 3.5 Sonnet', 11 | provider 12 | }, 13 | { 14 | 'name': 'claude-3-sonnet-20240229', 15 | 'displayName': 'Claude 3 Sonnet', 16 | provider 17 | }, 18 | { 19 | 'name': 'claude-3-opus-20240229', 20 | 'displayName': 'Claude 3 Opus', 21 | provider 22 | }, 23 | 24 | { 25 | 'name': 'claude-3-haiku-20240307', 26 | 'displayName': 'Claude 3 Haiku', 27 | provider 28 | } 29 | ] -------------------------------------------------------------------------------- /app/adapter/openai/models.ts: -------------------------------------------------------------------------------- 1 | import { LLMModel } from "@/app/adapter/interface" 2 | export const provider = { 3 | id: 'openai', 4 | providerName: 'Open AI', 5 | } 6 | 7 | export const modelList: LLMModel[] = [ 8 | { 9 | 'name': 'gpt-4o', 10 | 'displayName': 'GPT 4o', 11 | provider 12 | }, 13 | { 14 | 'name': 'gpt-4o-mini', 15 | 'displayName': 'GPT 4o mini', 16 | provider 17 | }, 18 | { 19 | 'name': 'gpt-3.5-turbo', 20 | 'displayName': 'GPT 3.5 Turbo', 21 | provider 22 | }, 23 | { 24 | 'name': 'gpt-4-turbo-preview', 25 | 'displayName': 'GPT 4 Turbo', 26 | provider 27 | }, 28 | { 29 | 'name': 'gpt-4-32k', 30 | 'displayName': 'GPT 4 32k', 31 | provider 32 | }, 33 | 34 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 这是一个基于 Next.js 和 AI 大模型的翻译小工具,特点: 2 | 3 | * 同时调用多个大模型返回的翻译结果,方便对比翻译文本的质量 4 | * 完全客户端调用,API Key 信息存储在本地,没有泄露风险 5 | * 当前支持 Open AI、 Claude、 Moonshot、文心一言 6 | 7 | 线上预览链接:https://hive-translate.vercel.app/ 8 | 9 | ![Screenshot](https://github.com/user-attachments/assets/0a9b69da-09bc-4216-8eae-d9d8c3c341d7) 10 | 11 | 12 | 13 | ## 本地运行 14 | 15 | ### 开发预览 16 | ```bash 17 | npm run dev 18 | # or 19 | yarn dev 20 | # or 21 | pnpm dev 22 | ``` 23 | 打开 [http://localhost:3000](http://localhost:3000) 即可预览。 24 | 25 | ### 本地运行 26 | 27 | ``` 28 | npm run build 29 | npm run start 30 | ``` 31 | 32 | ## 在 Vercel 上部署 33 | 点击下面的按钮,即可部署。 34 | 35 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/wuhaoworld/hive-translate&project-name=hive-translate) 36 | -------------------------------------------------------------------------------- /app/settings/providers/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import MoonshotSettings from "@/app/adapter/moonshot/settings"; 3 | import OpenAiSettings from "@/app/adapter/openai/settings"; 4 | import YiyanSettings from "@/app/adapter/yiyan/settings"; 5 | import ClaudeSettings from "@/app/adapter/claude/settings"; 6 | import { useTranslations } from 'next-intl'; 7 | 8 | export default function Component() { 9 | const t = useTranslations('Settings'); 10 | return ( 11 | <> 12 |
13 |
14 |

15 | {t('modelSettings')} 16 |

17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /locales/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "Common": { 3 | "settings": "设置" 4 | }, 5 | "HomePage": { 6 | "meta-description": "最好用的 AI 翻译工具", 7 | "aiTranslate": "AI 翻译", 8 | "paste": "粘贴", 9 | "clear": "清空", 10 | "translate": "翻译", 11 | "addProvider": "添加翻译服务", 12 | "resultPlaceholder": "翻译结果将展示在这里", 13 | "copy": "复制", 14 | "copied": "已复制", 15 | "please": "请", 16 | "config": "设置", 17 | "noProviderNotice": "尚未设置任何翻译服务,", 18 | "clickHere": "点此设置" 19 | }, 20 | "Settings": { 21 | "modelSettings": "模型设置", 22 | "status": "启用", 23 | "enabled": "已启用", 24 | "disabled": "未启用", 25 | "endpoint": "中转地址", 26 | "optional": "选填", 27 | "defaultModel": "翻译时默认使用的模型", 28 | "configGuide": "查看设置引导", 29 | "save": "保存", 30 | "cancel": "取消" 31 | }, 32 | "Language": { 33 | "auto": "自动识别", 34 | "english": "English", 35 | "simplifiedChinese": "中文(简体)", 36 | "traditionalChinese": "中文(繁体)", 37 | "japanese": "日文", 38 | "korean": "韩语", 39 | "french": "法语", 40 | "german": "德语", 41 | "spanish": "西班牙语" 42 | } 43 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hive-translate", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@ant-design/icons": "^5.5.1", 13 | "@ant-design/nextjs-registry": "^1.0.1", 14 | "@hello-pangea/dnd": "^17.0.0", 15 | "@vercel/analytics": "^1.3.1", 16 | "antd": "^5.22.5", 17 | "next": "^14.2.20", 18 | "next-intl": "^3.26.2", 19 | "react": "^18", 20 | "react-beautiful-dnd": "^13.1.1", 21 | "react-copy-to-clipboard": "^5.1.0", 22 | "react-dom": "^18", 23 | "react-markdown": "^9.0.1", 24 | "remark-gfm": "^4.0.0" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^20.16.9", 28 | "@types/react": "^18", 29 | "@types/react-beautiful-dnd": "^13.1.8", 30 | "@types/react-copy-to-clipboard": "^5.0.7", 31 | "@types/react-dom": "^18", 32 | "eslint": "^8", 33 | "eslint-config-next": "14.2.13", 34 | "postcss": "^8", 35 | "tailwindcss": "^3.4.1", 36 | "typescript": "^5" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "Common": { 3 | "settings": "設定" 4 | }, 5 | "HomePage": { 6 | "meta-description": "最も使いやすいAI翻訳ツール", 7 | "aiTranslate": "AI翻訳", 8 | "paste": "貼り付け", 9 | "clear": "クリア", 10 | "translate": "翻訳", 11 | "addProvider": "翻訳サービスを追加", 12 | "resultPlaceholder": "翻訳結果はここに表示されます", 13 | "copy": "コピー", 14 | "copied": "コピーしました", 15 | "please": "お願いします", 16 | "config": "設定", 17 | "noProviderNotice": "まだ翻訳サービスが設定されていません、", 18 | "clickHere": "ここをクリックして設定" 19 | }, 20 | "Settings": { 21 | "modelSettings": "モデル設定", 22 | "status": "有効にする", 23 | "enabled": "有効になりました", 24 | "disabled": "無効になりました", 25 | "endpoint": "中継アドレス", 26 | "optional": "選択入力", 27 | "defaultModel": "翻訳時にデフォルトで使用されるモデル", 28 | "configGuide": "設定ガイドを表示", 29 | "save": "保存", 30 | "cancel": "キャンセル" 31 | }, 32 | "Language": { 33 | "auto": "自動認識", 34 | "english": "English", 35 | "simplifiedChinese": "中国語(簡体字)", 36 | "traditionalChinese": "中国語(繁体字)", 37 | "japanese": "日本語", 38 | "korean": "韓国語", 39 | "french": "フランス語", 40 | "german": "ドイツ語", 41 | "spanish": "スペイン語" 42 | } 43 | } -------------------------------------------------------------------------------- /app/adapter/yiyan/models.ts: -------------------------------------------------------------------------------- 1 | import { LLMModel } from "@/app/adapter/interface" 2 | 3 | export const provider = { 4 | id: 'yiyan', 5 | providerName: '文心一言', 6 | } 7 | 8 | export const modelList: LLMModel[] = [ 9 | { 10 | 'name': 'ERNIE-Speed', 11 | 'displayName': 'ERNIE-Speed', 12 | 'apiUrl': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie_speed', 13 | provider 14 | }, 15 | { 16 | 'name': 'ERNIE-Lite', 17 | 'displayName': 'ERNIE-Lite', 18 | 'apiUrl': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant', 19 | provider 20 | }, 21 | { 22 | 'name': 'ERNIE-Bot 4.0', 23 | 'displayName': 'ERNIE-Bot 4.0', 24 | 'apiUrl': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions_pro', 25 | provider 26 | }, 27 | { 28 | 'name': 'ERNIE-Bot-8K', 29 | 'displayName': 'ERNIE-Bot-8K', 30 | 'apiUrl': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie_bot_8k', 31 | provider 32 | }, 33 | { 34 | 'name': 'ERNIE-Bot-turbo', 35 | 'displayName': 'ERNIE-Bot-turbo', 36 | 'apiUrl': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant', 37 | provider 38 | } 39 | ]; -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { AntdRegistry } from '@ant-design/nextjs-registry'; 2 | import type { Metadata } from "next"; 3 | import { Analytics } from '@vercel/analytics/react'; 4 | import { Header } from '@/components/header'; 5 | import { NextIntlClientProvider } from 'next-intl'; 6 | import { getLocale, getMessages } from 'next-intl/server'; 7 | import "./globals.css"; 8 | 9 | export const metadata: Metadata = { 10 | title: "AI Translate", 11 | description: "The best AI translation tool", 12 | icons: { 13 | icon: '/icon.png' 14 | }, 15 | }; 16 | 17 | export default async function RootLayout({ 18 | children, 19 | }: Readonly<{ 20 | children: React.ReactNode; 21 | }>) { 22 | const locale = await getLocale(); 23 | const messages = await getMessages(); 24 | return ( 25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 | {children} 33 |
34 |
35 |
36 | 37 |
38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Common": { 3 | "settings": "Settings" 4 | }, 5 | "HomePage": { 6 | "meta-description": "The best AI translation tool", 7 | "aiTranslate": "AI Translate", 8 | "paste": "Paste", 9 | "clear": "Clear", 10 | "translate": "Translate", 11 | "addProvider": "Add Translation Provider", 12 | "resultPlaceholder": "The translation results will be displayed here.", 13 | "copy": "Copy", 14 | "copied": "Copied", 15 | "please": "Please", 16 | "config": "Config", 17 | "noProviderNotice": "No translation provider have been set up yet, ", 18 | "clickHere": "Click here to set" 19 | }, 20 | "Settings": { 21 | "modelSettings": "Model Settings", 22 | "status": "Status", 23 | "enabled": "Enabled", 24 | "disabled": "Disabled", 25 | "endpoint": "Endpoint", 26 | "optional": "Optional", 27 | "defaultModel": "Default Model", 28 | "configGuide": "Configuration Guide", 29 | "save": "Save", 30 | "cancel": "Cancel" 31 | }, 32 | "Language": { 33 | "auto": "Detect Language", 34 | "english": "English", 35 | "simplifiedChinese": "Simplified Chinese", 36 | "traditionalChinese": "Traditional Chinese", 37 | "japanese": "Japanese", 38 | "korean": "Korean", 39 | "french": "French", 40 | "german": "German", 41 | "spanish": "Spanish" 42 | } 43 | } -------------------------------------------------------------------------------- /i18n/request.ts: -------------------------------------------------------------------------------- 1 | import { getRequestConfig } from 'next-intl/server'; 2 | import { headers } from 'next/headers'; 3 | 4 | export default getRequestConfig(async () => { 5 | 6 | const locales = ['en', 'zh', 'ja']; 7 | const defaultLocale = 'en'; 8 | 9 | const headersList = headers(); 10 | // 获取 cookie 中的语言设置 11 | const cookieLanguage = headersList.get('cookie')?.split(';') 12 | .map(cookie => cookie.trim()) 13 | .find(cookie => cookie.startsWith('language=')) 14 | ?.split('=')[1]; 15 | // 如果 cookie 中有有效的语言设置,直接使用 16 | if (cookieLanguage && locales.includes(cookieLanguage)) { 17 | return { 18 | locale: cookieLanguage, 19 | messages: (await import(`../locales/${cookieLanguage}.json`)).default 20 | }; 21 | } 22 | const acceptLanguage = headersList.get('accept-language') || ''; 23 | // 解析用户偏好的语言列表 24 | const userLanguages = acceptLanguage.split(',') 25 | .map(lang => { 26 | const [language, weight = 'q=1.0'] = lang.split(';'); 27 | return { 28 | language: language.split('-')[0], // 只取主要语言代码 29 | weight: parseFloat(weight.split('=')[1]) 30 | }; 31 | }) 32 | .sort((a, b) => b.weight - a.weight); 33 | 34 | // 查找第一个匹配的支持语言 35 | const matchedLocale = userLanguages.find( 36 | ({ language }) => locales.includes(language) 37 | ); 38 | const locale = matchedLocale ? matchedLocale.language : defaultLocale; 39 | return { 40 | locale, 41 | messages: (await import(`../locales/${locale}.json`)).default 42 | }; 43 | }); -------------------------------------------------------------------------------- /app/adapter/interface.ts: -------------------------------------------------------------------------------- 1 | export abstract class LLMApi { 2 | abstract chat(options: ChatOptions): Promise; 3 | abstract usage(): Promise; 4 | abstract models(): Promise; 5 | } 6 | 7 | export interface ChatOptions { 8 | messages: RequestMessage[]; 9 | config: LLMConfig; 10 | apiUrl?: string; 11 | apiKey?: string; 12 | onUpdate: (message: string) => void; 13 | onFinish: (message: string) => void; 14 | onError?: (err: Error) => void; 15 | onController?: (controller: AbortController) => void; 16 | } 17 | 18 | // 暂时只支持文本 19 | export interface RequestMessage { 20 | role: 'user' | 'assistant' | 'system'; 21 | content: string; 22 | } 23 | 24 | export interface LLMConfig { 25 | model: string; 26 | temperature?: number; 27 | top_p?: number; 28 | stream?: boolean; 29 | presence_penalty?: number; 30 | frequency_penalty?: number; 31 | } 32 | 33 | export interface LLMUsage { 34 | used: number; 35 | total: number; 36 | } 37 | 38 | export interface LLMModel { 39 | name: string; 40 | displayName: string; 41 | apiUrl?: string; 42 | // available: boolean; 43 | provider: LLMModelProvider; 44 | } 45 | 46 | // export interface LLMModel { 47 | // name: string; 48 | // available: boolean; 49 | // provider: LLMModelProvider; 50 | // } 51 | 52 | export interface LLMModelProvider { 53 | id: string; 54 | providerName: string; 55 | status?: boolean 56 | // providerType: string; 57 | } 58 | 59 | export default interface TranslaterComponent { 60 | startTranslate: (question: string, language: string, completeCallback: (result: string) => void) => void; 61 | stopTranslate: () => void; 62 | clear: () => void; 63 | } -------------------------------------------------------------------------------- /app/adapter/claude/claude_logo.svg: -------------------------------------------------------------------------------- 1 | Claude -------------------------------------------------------------------------------- /app/adapter/claude/utils.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { ChatOptions } from '@/app/adapter/interface' 3 | import { RequestMessage } from '@/app/adapter/interface' 4 | import { CladueApi } from './api'; 5 | export function translate( 6 | fromLanguage: string, 7 | toLanguage: string, 8 | text: string, 9 | model: string, 10 | onUpdateMessage: (text: string) => void, 11 | onFinish?: (text: string) => void, 12 | onError?: (text?: string) => void, 13 | ) { 14 | const apikey = localStorage.getItem('claude_api_key') || ''; 15 | const apiUrl = localStorage.getItem('claude_proxy_url') || ''; 16 | const bot = new CladueApi(); 17 | let messages: RequestMessage[]; 18 | if (fromLanguage.toLowerCase() === 'auto') { 19 | messages = [ 20 | { 21 | 'role': 'user', 'content': `Translate the following source text to ${toLanguage}, Output translation directly without any additional text. 22 | Source Text: ${text} 23 | Translated Text:` }] 24 | } else { 25 | messages = [ 26 | { 27 | 'role': 'user', 'content': `Translate the following source text from ${fromLanguage} to ${toLanguage}, Output translation directly without any additional text. 28 | Source Text: ${text}. 29 | Translated Text:` }] 30 | } 31 | const options: ChatOptions = { 32 | messages: messages, 33 | config: { model: model }, 34 | apiKey: apikey, 35 | apiUrl: apiUrl, 36 | onUpdate: (message: string) => { 37 | onUpdateMessage(message) 38 | }, 39 | onFinish: async (message: string) => { 40 | if (onFinish) { 41 | onFinish(message) 42 | } 43 | }, 44 | onError: (err: Error) => { 45 | if (onError) { // 检查 onError 是否已定义 46 | onError(err.message) 47 | } 48 | }, 49 | onController: (controller: AbortController) => { 50 | // console.log("controller", controller) 51 | } 52 | } 53 | bot.chat(options); 54 | } 55 | -------------------------------------------------------------------------------- /app/adapter/moonshot/utils.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { ChatOptions } from '@/app/adapter/interface' 3 | import { RequestMessage } from '@/app/adapter/interface' 4 | import { MoonshotApi } from './api'; 5 | export function translate( 6 | fromLanguage: string, 7 | toLanguage: string, 8 | text: string, 9 | model:string, 10 | onUpdateMessage: (text:string) => void, 11 | onFinish?: (text: string) => void, 12 | onError?: (text?:string) => void, 13 | ) { 14 | const apikey = localStorage.getItem('moonshot_api_key') || ''; 15 | const bot = new MoonshotApi(); 16 | let messages: RequestMessage[]; 17 | if (fromLanguage.toLowerCase() === 'auto') { 18 | messages = [ 19 | { 'role': 'system', 'content': `You are a professional, authentic machine translation engine.` }, 20 | { 21 | 'role': 'user', 'content': `Translate the following source text to ${toLanguage}, Output translation directly without any additional text. 22 | Source Text: ${text} 23 | Translated Text:` }] 24 | } else { 25 | messages = [ 26 | { 'role': 'system', 'content': `You are a professional, authentic machine translation engine.` }, 27 | { 28 | 'role': 'user', 'content': `Translate the following source text from ${fromLanguage} to ${toLanguage}, Output translation directly without any additional text. 29 | Source Text: ${text}. 30 | Translated Text:` }] 31 | } 32 | 33 | const options: ChatOptions = { 34 | messages: messages, 35 | config: { model: model }, 36 | apiKey: apikey, 37 | onUpdate: (message: string) => { 38 | onUpdateMessage(message) 39 | }, 40 | onFinish: async (message: string) => { 41 | if (onFinish) { 42 | onFinish(message) 43 | } 44 | }, 45 | onError: (err: Error) => { 46 | if (onError) { // 检查 onError 是否已定义 47 | onError(err.message) 48 | } 49 | }, 50 | onController: (controller: AbortController) => { 51 | // console.log("controller", controller) 52 | } 53 | } 54 | bot.chat(options); 55 | } 56 | 57 | -------------------------------------------------------------------------------- /app/adapter/openai/utils.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { ChatOptions } from '@/app/adapter/interface' 3 | import { RequestMessage } from '@/app/adapter/interface' 4 | import { ChatGPTApi } from './api'; 5 | export function translate( 6 | fromLanguage: string, 7 | toLanguage: string, 8 | text: string, 9 | model : string, 10 | onUpdateMessage: (text: string) => void, 11 | onFinish?: (text: string) => void, 12 | onError?: (text?: string) => void, 13 | ) { 14 | const apikey = localStorage.getItem('openai_api_key') || ''; 15 | const apiUrl = localStorage.getItem('openai_proxy_url') || ''; 16 | const bot = new ChatGPTApi(); 17 | let messages: RequestMessage[]; 18 | if (fromLanguage.toLowerCase() === 'auto') { 19 | messages = [ 20 | { 'role': 'system', 'content': `You are a professional, authentic machine translation engine.` }, 21 | { 22 | 'role': 'user', 'content': `Translate the following source text to ${toLanguage}, Output translation directly without any additional text. 23 | Source Text: ${text} 24 | Translated Text:` }] 25 | } else { 26 | messages = [ 27 | { 'role': 'system', 'content': `You are a professional, authentic machine translation engine.` }, 28 | { 29 | 'role': 'user', 'content': `Translate the following source text from ${fromLanguage} to ${toLanguage}, Output translation directly without any additional text. 30 | Source Text: ${text}. 31 | Translated Text:` }] 32 | } 33 | 34 | const options: ChatOptions = { 35 | messages: messages, 36 | config: { model: model }, 37 | apiKey: apikey, 38 | apiUrl: apiUrl, 39 | onUpdate: (message: string) => { 40 | onUpdateMessage(message) 41 | }, 42 | onFinish: async (message: string) => { 43 | if (onFinish) { 44 | onFinish(message) 45 | } 46 | }, 47 | onError: (err: Error) => { 48 | if (onError) { // 检查 onError 是否已定义 49 | onError(err.message) 50 | } 51 | }, 52 | onController: (controller: AbortController) => { 53 | // console.log("controller", controller) 54 | } 55 | } 56 | bot.chat(options); 57 | } 58 | 59 | -------------------------------------------------------------------------------- /app/adapter/yiyan/utils.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { ChatOptions } from '@/app/adapter/interface' 3 | import { RequestMessage } from '@/app/adapter/interface' 4 | import { YiyanApi } from './api'; 5 | import { modelList } from './models'; 6 | export function translate( 7 | fromLanguage: string, 8 | toLanguage: string, 9 | text: string, 10 | model: string, 11 | onUpdateMessage: (text: string) => void, 12 | onFinish?: (text: string) => void, 13 | onError?: (text?: string) => void, 14 | ) { 15 | const apikey = localStorage.getItem('yiyan_api_key') || ''; 16 | const apisecret = localStorage.getItem('yiyan_api_secret') || ''; 17 | const bot = new YiyanApi(); 18 | const modelObj = modelList.filter(item => item.name === model); 19 | let apiUrl; 20 | if (modelObj.length > 1) { 21 | apiUrl = modelObj[0]['apiUrl']; 22 | } else { 23 | apiUrl = modelList[0]['apiUrl']; 24 | } 25 | 26 | let messages: RequestMessage[]; 27 | if (fromLanguage.toLowerCase() === 'auto') { 28 | messages = [ 29 | { 30 | 'role': 'user', 'content': `Translate the following source text to ${toLanguage}, Output translation directly without any additional text. 31 | Source Text: ${text} 32 | Translated Text:` }] 33 | } else { 34 | messages = [ 35 | { 36 | 'role': 'user', 'content': `Translate the following source text from ${fromLanguage} to ${toLanguage}, Output translation directly without any additional text. 37 | Source Text: ${text}. 38 | Translated Text:` }] 39 | } 40 | 41 | const options: ChatOptions = { 42 | messages: messages, 43 | config: { model: model }, 44 | apiKey: `${apikey}-${apisecret}`, 45 | apiUrl: apiUrl, 46 | onUpdate: (message: string) => { 47 | onUpdateMessage(message) 48 | }, 49 | onFinish: async (message: string) => { 50 | if (onFinish) { 51 | onFinish(message) 52 | } 53 | }, 54 | onError: (err: Error) => { 55 | if (onError) { // 检查 onError 是否已定义 56 | onError(err.message) 57 | } 58 | }, 59 | onController: (controller: AbortController) => { 60 | // console.log("controller", controller) 61 | } 62 | } 63 | bot.chat(options); 64 | } 65 | 66 | -------------------------------------------------------------------------------- /app/adapter/claude/claude_text.svg: -------------------------------------------------------------------------------- 1 | Claude -------------------------------------------------------------------------------- /components/header.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useEffect, useState } from 'react'; 3 | import Image from 'next/image'; 4 | import Link from 'next/link' 5 | import { Button, Select } from 'antd'; 6 | import { useTranslations } from 'next-intl'; 7 | import { SettingOutlined, TranslationOutlined } from '@ant-design/icons'; 8 | import logo from '@/app/images/logo.svg'; 9 | export function Header() { 10 | const t = useTranslations('Common'); 11 | const [currentLang, setCurrentLang] = useState('zh'); 12 | 13 | useEffect(() => { 14 | // 从 cookie 中获取语言设置 15 | const getCookie = (name: string) => { 16 | const value = `; ${document.cookie}`; 17 | const parts = value.split(`; ${name}=`); 18 | if (parts.length === 2) return parts.pop()?.split(';').shift(); 19 | return undefined; 20 | }; 21 | 22 | // 获取浏览器语言 23 | const getBrowserLanguage = () => { 24 | const lang = navigator.language.toLowerCase(); 25 | if (lang.startsWith('zh')) return 'zh'; 26 | if (lang.startsWith('ja')) return 'ja'; 27 | return 'en'; // 默认返回英文 28 | }; 29 | 30 | // 设置当前语言 31 | const savedLang = getCookie('language'); 32 | if (savedLang && ['zh', 'en', 'ja'].includes(savedLang)) { 33 | setCurrentLang(savedLang); 34 | } else { 35 | const browserLang = getBrowserLanguage(); 36 | setCurrentLang(browserLang); 37 | document.cookie = `language=${browserLang}; path=/`; 38 | } 39 | }, []); 40 | 41 | return ( 42 |
43 |
44 |

45 | 46 | Logo 47 | Hive Translate 48 | 49 |

50 |
51 |
52 | 138 | 139 | 140 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 150 | 151 | 152 | 153 | 154 | 155 | 213 |