Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added i18n #106

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions next-i18next.config.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
i18n: {
locales: ['en', 'zh-CN'], // Supported Languages
defaultLocale: 'en', // Default language
},
};

5,764 changes: 3,759 additions & 2,005 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,29 @@
"@radix-ui/react-tooltip": "^1.1.2",
"@tanstack/react-query": "^5.62.15",
"@types/dom-speech-recognition": "^0.0.4",
"ai": "^4.0.33",
"ai": "^4.1.21",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"emoji-mart": "^5.6.0",
"framer-motion": "^11.5.6",
"i18next": "^24.2.2",
"i18next-browser-languagedetector": "^8.0.2",
"i18next-http-backend": "^3.0.2",
"langchain": "^0.3.2",
"lodash": "^4.17.21",
"lucide-react": "^0.445.0",
"next": "^14.2.13",
"next-i18next": "^15.4.1",
"next-themes": "^0.3.0",
"ollama-ai-provider": "^0.15.0",
"react": "^18.3.1",
"react-code-blocks": "^0.1.6",
"react-dom": "^18.3.1",
"react-dropzone": "^14.2.9",
"react-hook-form": "^7.53.0",
"react-i18next": "^15.4.0",
"react-markdown": "^9.0.1",
"react-redux": "^9.2.0",
"react-resizable-panels": "^2.1.3",
"react-textarea-autosize": "^8.5.3",
"remark-gfm": "^4.0.0",
Expand Down
51 changes: 51 additions & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"Home": {
"welcome_title": "Welcome to Ollama!",
"welcome_description": "Enter your name to get started. This is just to personalize your experience.",
"anonymous": "Anonymous"
},
"chat": {
"help_welcome_message": "How can I help you today?",
"input_prompt_placeholder": "Enter your prompt here",
"loading": "Loading...",
"model_select": "Select model",
"no_models_available": "No models available",
"thinking_process": "Thinking process",
"new_chat": "New Chat",
"user_chats": "Your Chats",
"delete_chat": "Delete chat",
"delete_confirmation_title": "Are you sure you want to delete this chat?",
"delete_confirmation_description": "This action cannot be undone.",
"cancel": "Cancel",
"delete": "Delete"

},

"theme": {
"theme":"Theme",
"system": "System",
"dark": "Dark",
"light": "Light"
},
"model": {
"model_select": "Select model",
"model_pull_success": "Model pulled successfully",
"status": "Status",
"pulling": "Pulling",
"pull_model": "Pull model",
"start_pull_model": "Start Pull Model",
"library_check": "Check the",
"library_link": "library",
"download_in_progress": "This may take a while. You can safely close this modal and continue using the app.",
"download_info": "Pressing the button will download the specified model to your device."
},
"name": "Name",
"change_name": "Change name",
"drop_images_here": "Drop the images here",
"input_name": "Enter your name",
"user_name": "username",
"user_name_updated_successfully": "Name updated successfully",
"settings": "Settings"

}

47 changes: 47 additions & 0 deletions public/locales/zh-CN/translation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"Home": {
"welcome_title": "欢迎来到 Ollama UI!",
"welcome_description": "为了您的个性化体验,请输入您的用户名。",
"anonymous": "匿名用户"
},
"chat": {
"help_welcome_message": "今天有需要我帮忙的吗?",
"input_prompt_placeholder": "在这里输入。",
"loading": "加载中...",
"model_select": "选择模型",
"no_models_available": "没有可用的模型",
"thinking_process": "思考中...",
"new_chat": "新的对话",
"user_chats": "你的对话记录",
"delete_chat": "删除对话",
"delete_confirmation_title": "您确定要删除此对话吗?",
"delete_confirmation_description": "此操作无法撤销。",
"cancel": "取消",
"delete": "删除"
},
"model": {
"model_select": "选择模型",
"model_pull_success": "模型下载成功",
"status": "状态",
"pulling": "下载",
"pull_model": "下载模型",
"start_pull_model": "开始下载模型",
"library_check": "查看",
"library_link": "列表",
"download_in_progress": "这可能需要一段时间,您可以安全地关闭此框并继续忙其他事。",
"download_info": "按下按钮将下载指定的模型到您的设备。"
},
"theme": {
"theme":"主题",
"system": "跟随系统",
"dark": "暗色",
"light": "亮色"
},
"name": "用户",
"change_name": "更改用户名",
"drop_images_here": "将图片拖到这里",
"input_name": "请输入您的姓名",
"user_name": "用户名",
"user_name_updated_successfully": "用户名更新成功",
"settings": "设置"
}
18 changes: 10 additions & 8 deletions src/app/(chat)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { Inter } from "next/font/google";
import "../globals.css";
import { ThemeProvider } from "@/providers/theme-provider";
import { Toaster } from "@/components/ui/sonner";
import { dir } from "i18next"; // Get text direction (ltr/rtl)
import { languages, defaultLanguage } from "@/lib/i18n"; // i18n configuration
import { cookies } from "next/headers"; // Retrieve cookies on the server side

const inter = Inter({ subsets: ["latin"] });

Expand All @@ -11,20 +14,19 @@ export const metadata: Metadata = {
description: "Ollama chatbot web interface",
};

export const viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: 1,
};

export default function RootLayout({
children,
params,
}: Readonly<{
children: React.ReactNode;
params: { locale: string }; // Dynamic locale
}>) {
// Get current language (default to "en" if not set)
const cookieStore = cookies();
const lang = cookieStore.get("NEXT_LOCALE")?.value || defaultLanguage;

return (
<html lang="en">
<html lang={lang}>
<body className={`antialiased tracking-tight ${inter.className}`}>
<ThemeProvider attribute="class" defaultTheme="dark">
{children}
Expand Down
10 changes: 6 additions & 4 deletions src/app/(chat)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import UsernameForm from "@/components/username-form";
import { generateUUID } from "@/lib/utils";
import React from "react";
import useChatStore from "../hooks/useChatStore";
import { useTranslation } from "react-i18next"; // 导入 i18n hook

export default function Home() {
const { t } = useTranslation("translation"); // 使用 i18n 获取翻译
const id = generateUUID();
const [open, setOpen] = React.useState(false);
const userName = useChatStore((state) => state.userName);
Expand All @@ -22,7 +24,7 @@ export default function Home() {
const onOpenChange = (isOpen: boolean) => {
if (userName) return setOpen(isOpen);

setUserName("Anonymous");
setUserName(t("Home.anonymous"));
setOpen(isOpen);
};

Expand All @@ -38,10 +40,10 @@ export default function Home() {
/>
<DialogContent className="flex flex-col space-y-4">
<DialogHeader className="space-y-2">
<DialogTitle>Welcome to Ollama!</DialogTitle>
{/* 使用翻译文件中的 "Home" 分类键 */}
<DialogTitle>{t("Home.welcome_title")}</DialogTitle>
<DialogDescription>
Enter your name to get started. This is just to personalize your
experience.
{t("Home.welcome_description")}
</DialogDescription>
<UsernameForm setOpen={setOpen} />
</DialogHeader>
Expand Down
1 change: 0 additions & 1 deletion src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createOllama } from 'ollama-ai-provider';
import { streamText, convertToCoreMessages, CoreMessage, UserContent } from 'ai';

export const runtime = "edge";
export const dynamic = "force-dynamic";

export async function POST(req: Request) {
Expand Down
3 changes: 2 additions & 1 deletion src/app/hooks/useSpeechRecognition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const useSpeechToText = (options: SpeechRecognitionOptions = {}) => {
const [isListening, setIsListening] = useState(false);
const [transcript, setTranscript] = useState("");
const recognitionRef = useRef<SpeechRecognition | null>(null);
const optionsRef = useRef(options);

useEffect(() => {
if (!("webkitSpeechRecognition" in window)) {
Expand Down Expand Up @@ -57,7 +58,7 @@ const useSpeechToText = (options: SpeechRecognitionOptions = {}) => {
recognitionRef.current.stop();
}
};
}, []);
}, [options]);

const startListening = () => {
if (recognitionRef.current && !isListening) {
Expand Down
5 changes: 4 additions & 1 deletion src/components/chat/chat-bottombar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import useChatStore from "@/app/hooks/useChatStore";
import Image from "next/image";
import { ChatRequestOptions, Message } from "ai";
import { ChatInput } from "../ui/chat/chat-input";
import { useTranslation } from "react-i18next"; // 导入 i18n hook

interface ChatBottombarProps {
handleInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
Expand Down Expand Up @@ -76,6 +77,8 @@ export default function ChatBottombar({
}
}, [inputRef]);

const { t } = useTranslation("translation");

return (
<div className="px-4 pb-7 flex justify-between w-full items-center relative ">
<AnimatePresence initial={false}>
Expand All @@ -89,7 +92,7 @@ export default function ChatBottombar({
onKeyDown={handleKeyPress}
onChange={handleInputChange}
name="message"
placeholder={!isListening ? "Enter your prompt here" : "Listening"}
placeholder={!isListening ? t("chat.input_prompt_placeholder") : "Listening"}
className="max-h-40 px-6 pt-6 border-0 shadow-none bg-accent rounded-lg text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed dark:bg-card"
/>

Expand Down
1 change: 1 addition & 0 deletions src/components/chat/chat-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Message, useChat } from "ai/react";
import Chat, { ChatProps } from "./chat";
import ChatList from "./chat-list";
import { HamburgerMenuIcon } from "@radix-ui/react-icons";
import { useTranslation } from "react-i18next";

interface ChatLayoutProps {
defaultLayout: number[] | undefined;
Expand Down
1 change: 1 addition & 0 deletions src/components/chat/chat-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ChatBubbleMessage,
} from "../ui/chat/chat-bubble";
import { ChatRequestOptions } from "ai";
import { useTranslation } from "react-i18next";

interface ChatListProps {
messages: Message[];
Expand Down
5 changes: 4 additions & 1 deletion src/components/chat/chat-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import ButtonWithTooltip from "../button-with-tooltip";
import { Button } from "../ui/button";
import CodeDisplayBlock from "../code-display-block";
import { useTranslation } from "react-i18next";

export type ChatMessageProps = {
message: Message;
Expand Down Expand Up @@ -78,11 +79,13 @@ function ChatMessage({ message, isLast, isLoading, reload }: ChatMessageProps) {
</div>
);

const { t } = useTranslation("translation"); // 使用 i18n 获取翻译

const renderThinkingProcess = () => (
thinkContent && message.role === "assistant" && (
<details className="mb-2 text-sm" open>
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
Thinking process
{t("chat.thinking_process")}
</summary>
<div className="mt-2 text-muted-foreground">
<Markdown remarkPlugins={[remarkGfm]}>{thinkContent}</Markdown>
Expand Down
22 changes: 10 additions & 12 deletions src/components/chat/chat-topbar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import {
Popover,
PopoverContent,
Expand All @@ -9,9 +9,6 @@ import {
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";

Expand All @@ -21,6 +18,7 @@ import { Sidebar } from "../sidebar";
import { Message } from "ai/react";
import { getSelectedModel } from "@/lib/model-helper";
import useChatStore from "@/app/hooks/useChatStore";
import { useTranslation } from "react-i18next";

interface ChatTopbarProps {
isLoading: boolean;
Expand All @@ -35,29 +33,29 @@ export default function ChatTopbar({
messages,
setMessages,
}: ChatTopbarProps) {
const [models, setModels] = React.useState<string[]>([]);
const [open, setOpen] = React.useState(false);
const [sheetOpen, setSheetOpen] = React.useState(false);
const [models, setModels] = useState<string[]>([]);
const [open, setOpen] = useState(false);
const [sheetOpen, setSheetOpen] = useState(false);
const selectedModel = useChatStore((state) => state.selectedModel);
const setSelectedModel = useChatStore((state) => state.setSelectedModel);
const { t } = useTranslation("translation");
const [isClient, setIsClient] = useState(false); // 客户端标记

useEffect(() => {
setIsClient(true); // 在客户端渲染时设置 isClient
(async () => {
try {
const res = await fetch("/api/tags");
if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);

const data = await res.json().catch(() => null);
if (!data?.models?.length) return;

setModels(data.models.map(({ name }: { name: string }) => name));
} catch (error) {
console.error("Error fetching models:", error);
}
})();
}, []);


const handleModelChange = (model: string) => {
setSelectedModel(model);
setOpen(false);
Expand Down Expand Up @@ -93,7 +91,7 @@ export default function ChatTopbar({
aria-expanded={open}
className="w-[300px] justify-between"
>
{selectedModel || "Select model"}
{selectedModel || (isClient ? t("chat.model_select") : "chat.model_select")}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
Expand All @@ -113,7 +111,7 @@ export default function ChatTopbar({
))
) : (
<Button variant="ghost" disabled className=" w-full">
No models available
{t("chat.no_models_available")}
</Button>
)}
</PopoverContent>
Expand Down
Loading