mirror of
https://github.com/labring/FastGPT.git
synced 2026-02-27 01:02:22 +08:00
feat(i18n): Fix language loss in navigation and add language selector (#6467)
* feat(docs): enable i18n language selector * docs(i18n): translate introduction page to English * fix(i18n): fix language switching issue by always showing locale prefix * fix(docs): use relative paths for internal links to preserve language * refactor(i18n): add getLocalizedPath helper to simplify URL generation * refactor(i18n): make getLocalizedPath respect hideLocale config * feat(i18n): fallback to default language when translation missing, keep URL unchanged * feat(i18n): fix language loss in navigation and add language selector - Set hideLocale to 'never' to always show language prefix - Add localized-navigation.ts with useLocalizedRouter hook - Update all navigation points to preserve language: 1. Tab navigation (already using getLocalizedPath) 2. Sidebar navigation (handled by Fumadocs) 3. Home/404 redirects (using getLocalizedPath) 4. MDX Redirect component (using useLocalizedRouter) 5. Old page redirects (updated not-found.tsx) 6. Document links (custom LocalizedLink component) - Configure language selector in layout.config.tsx - Add LOCALIZED_NAVIGATION.md documentation * fix(i18n): fix type errors and useEffect dependencies * refactor(i18n): move redirects to middleware for SSR support - Move old path redirects from client-side (not-found.tsx) to server-side (middleware.ts) - Use 301 permanent redirects for better SEO - Preserve language prefix in redirects - Fix SSR issue caused by client-side redirects * refactor(i18n): clean up not-found.tsx, remove duplicate redirect maps - Remove duplicate exactMap and prefixMap (now in middleware) - Keep dynamic meta.json lookup for unknown pages - Simplify to only handle fallback cases - Two-layer approach: middleware (SSR) + not-found (dynamic) * refactor(i18n): simplify not-found to always redirect to introduction - Remove dynamic meta.json lookup - Always redirect to introduction page on 404 - Ensures no 404 pages are shown - Keep language prefix in redirect * fix(i18n): fix middleware type error with ts-expect-error - Add @ts-expect-error for Fumadocs middleware signature mismatch - Fix syntax error in config matcher (remove literal \n) --------- Co-authored-by: archer <archer@archerdeMac-mini.local>
This commit is contained in:
107
document/LOCALIZED_NAVIGATION.md
Normal file
107
document/LOCALIZED_NAVIGATION.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# 国际化路由适配说明
|
||||
|
||||
## 问题
|
||||
路由跳转时语言前缀丢失,导致用户在切换页面后回到默认语言。
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 1. 核心配置
|
||||
- **`lib/i18n.ts`**: 设置 `hideLocale: 'never'`,确保所有语言(包括默认语言)都显示语言前缀
|
||||
- **`lib/localized-navigation.ts`**: 提供客户端路由工具,自动处理语言前缀
|
||||
|
||||
### 2. 修复的路由跳转位置
|
||||
|
||||
#### ✅ 一级导航(Tab Navigation)
|
||||
- **文件**: `app/[lang]/docs/layout.tsx`
|
||||
- **方法**: 使用 `getLocalizedPath()` 为每个 tab 的 URL 添加语言前缀
|
||||
- **状态**: 已完成
|
||||
|
||||
#### ✅ 二级导航(Sidebar)
|
||||
- **文件**: `lib/source.ts`
|
||||
- **方法**: Fumadocs 的 loader 自动根据 i18n 配置生成带语言前缀的链接
|
||||
- **状态**: 已完成(无需修改)
|
||||
|
||||
#### ✅ 空页面重定向
|
||||
- **文件**:
|
||||
- `app/[lang]/(home)/page.tsx` - 首页重定向
|
||||
- `app/[lang]/(home)/[...not-found]/page.tsx` - 404 重定向
|
||||
- **方法**: 使用 `getLocalizedPath()` 添加语言前缀
|
||||
- **状态**: 已完成
|
||||
|
||||
#### ✅ MDX 组件重定向
|
||||
- **文件**: `components/docs/Redirect.tsx`
|
||||
- **方法**: 使用 `useLocalizedRouter()` hook
|
||||
- **状态**: 已完成
|
||||
|
||||
#### ✅ 旧页面重定向
|
||||
- **文件**: `components/docs/not-found.tsx`
|
||||
- **方法**:
|
||||
- 使用 `useCurrentLang()` 获取当前语言
|
||||
- 从 pathname 中移除语言前缀进行匹配
|
||||
- 使用 `useLocalizedPath()` 为重定向目标添加语言前缀
|
||||
- **状态**: 已完成
|
||||
|
||||
#### ✅ 文档内链接
|
||||
- **文件**:
|
||||
- `components/docs/LocalizedLink.tsx` - 自定义 Link 组件
|
||||
- `mdx-components.tsx` - 配置 MDX 使用 LocalizedLink
|
||||
- **方法**: 拦截 MDX 中的 `<a>` 标签,自动为内部链接添加语言前缀
|
||||
- **状态**: 已完成
|
||||
|
||||
### 3. 语言选择器
|
||||
- **文件**: `app/layout.config.tsx`
|
||||
- **配置**:
|
||||
```typescript
|
||||
i18n: {
|
||||
locale,
|
||||
languages: [
|
||||
{ name: '简体中文', locale: 'zh-CN' },
|
||||
{ name: 'English', locale: 'en' }
|
||||
],
|
||||
hideLocale: 'never'
|
||||
}
|
||||
```
|
||||
- **状态**: 已完成
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 客户端组件
|
||||
```tsx
|
||||
'use client';
|
||||
import { useLocalizedRouter } from '@/lib/localized-navigation';
|
||||
|
||||
function MyComponent() {
|
||||
const router = useLocalizedRouter();
|
||||
router.push('/docs/introduction'); // 自动添加语言前缀
|
||||
}
|
||||
```
|
||||
|
||||
### 服务端组件
|
||||
```tsx
|
||||
import { getLocalizedPath } from '@/lib/i18n';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function Page({ params }: { params: Promise<{ lang: string }> }) {
|
||||
const { lang } = await params;
|
||||
redirect(getLocalizedPath('/docs/intro', lang));
|
||||
}
|
||||
```
|
||||
|
||||
### MDX 文档
|
||||
```mdx
|
||||
<!-- 内部链接会自动添加语言前缀 -->
|
||||
[查看介绍](/docs/introduction)
|
||||
|
||||
<!-- 或使用 Redirect 组件 -->
|
||||
<Redirect to="/docs/faq" />
|
||||
```
|
||||
|
||||
## 测试清单
|
||||
- [ ] 一级导航切换保持语言
|
||||
- [ ] 侧边栏导航保持语言
|
||||
- [ ] 首页重定向保持语言
|
||||
- [ ] 404 页面重定向保持语言
|
||||
- [ ] 旧链接重定向保持语言
|
||||
- [ ] 文档内链接保持语言
|
||||
- [ ] 语言选择器正常工作
|
||||
- [ ] 搜索结果链接保持语言
|
||||
@@ -1,5 +1,11 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import { getLocalizedPath } from '@/lib/i18n';
|
||||
|
||||
export default function HomePage() {
|
||||
redirect(`/docs/introduction`);
|
||||
export default async function NotFoundPage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ lang: string }>;
|
||||
}) {
|
||||
const { lang } = await params;
|
||||
redirect(getLocalizedPath('/docs/introduction', lang));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import { getLocalizedPath } from '@/lib/i18n';
|
||||
|
||||
export default function HomePage() {
|
||||
redirect(`/docs/introduction`);
|
||||
export default async function HomePage({
|
||||
params
|
||||
}: {
|
||||
params: Promise<{ lang: string }>;
|
||||
}) {
|
||||
const { lang } = await params;
|
||||
redirect(getLocalizedPath('/docs/introduction', lang));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { notFound } from 'next/navigation';
|
||||
import NotFound from '@/components/docs/not-found';
|
||||
import { createRelativeLink } from 'fumadocs-ui/mdx';
|
||||
import { getMDXComponents } from '@/mdx-components';
|
||||
import { i18n } from '@/lib/i18n';
|
||||
|
||||
// 在构建时导入静态数据
|
||||
import docLastModifiedData from '@/data/doc-last-modified.json';
|
||||
@@ -14,8 +15,16 @@ export default async function Page({
|
||||
params: Promise<{ lang: string; slug?: string[] }>;
|
||||
}) {
|
||||
const { lang, slug } = await params;
|
||||
const page = source.getPage(slug, lang);
|
||||
let page = source.getPage(slug, lang);
|
||||
|
||||
// If page not found in current language, fallback to default language
|
||||
// This allows showing Chinese content when English translation is not available
|
||||
// while keeping the URL unchanged (e.g., /en/docs/faq shows Chinese content)
|
||||
if (!page || !page.data || !page.file) {
|
||||
page = source.getPage(slug, i18n.defaultLanguage);
|
||||
}
|
||||
|
||||
// If still not found in default language, show 404
|
||||
if (!page || !page.data || !page.file) {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type ReactNode } from 'react';
|
||||
import { source } from '@/lib/source';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/notebook';
|
||||
import { baseOptions } from '@/app/layout.config';
|
||||
import { t } from '@/lib/i18n';
|
||||
import { t, getLocalizedPath, i18n } from '@/lib/i18n';
|
||||
import LogoLight from '@/components/docs/logo';
|
||||
import LogoDark from '@/components/docs/logoDark';
|
||||
import '@/app/global.css';
|
||||
@@ -28,32 +28,32 @@ export default async function Layout({
|
||||
{
|
||||
icon: <BookOpen className={iconClass} />,
|
||||
title: t('common:introduction', lang),
|
||||
url: lang === 'zh-CN' ? '/docs/introduction' : '/en/docs/introduction'
|
||||
url: getLocalizedPath('/docs/introduction', lang)
|
||||
},
|
||||
{
|
||||
icon: <Code className={iconClass} />,
|
||||
title: t('common:api_reference', lang),
|
||||
url: lang === 'zh-CN' ? '/docs/openapi' : '/en/docs/openapi'
|
||||
url: getLocalizedPath('/docs/openapi', lang)
|
||||
},
|
||||
{
|
||||
icon: <Lightbulb className={iconClass} />,
|
||||
title: t('common:use-cases', lang),
|
||||
url: lang === 'zh-CN' ? '/docs/use-cases' : '/en/docs/use-cases'
|
||||
url: getLocalizedPath('/docs/use-cases', lang)
|
||||
},
|
||||
{
|
||||
icon: <CircleHelp className={iconClass} />,
|
||||
title: t('common:faq', lang),
|
||||
url: lang === 'zh-CN' ? '/docs/faq' : '/en/docs/faq'
|
||||
url: getLocalizedPath('/docs/faq', lang)
|
||||
},
|
||||
{
|
||||
icon: <Scale className={iconClass} />,
|
||||
title: t('common:protocol', lang),
|
||||
url: lang === 'zh-CN' ? '/docs/protocol' : '/en/docs/protocol'
|
||||
url: getLocalizedPath('/docs/protocol', lang)
|
||||
},
|
||||
{
|
||||
icon: <History className={iconClass} />,
|
||||
title: t('common:upgrading', lang),
|
||||
url: lang === 'zh-CN' ? '/docs/upgrading' : '/en/docs/upgrading'
|
||||
url: getLocalizedPath('/docs/upgrading', lang)
|
||||
}
|
||||
];
|
||||
|
||||
@@ -107,7 +107,7 @@ export default async function Layout({
|
||||
text: 'github'
|
||||
}
|
||||
]}
|
||||
tree={source.pageTree[lang]}
|
||||
tree={source.pageTree[lang] || source.pageTree[i18n.defaultLanguage]}
|
||||
searchToggle={{
|
||||
enabled: true
|
||||
}}
|
||||
|
||||
@@ -23,11 +23,7 @@ export const baseOptions = (locale: string): BaseLayoutProps => {
|
||||
</div>
|
||||
)
|
||||
},
|
||||
// i18n: {
|
||||
// languages: ['zh-CN', 'en'],
|
||||
// defaultLanguage: 'zh-CN',
|
||||
// hideLocale: 'always'
|
||||
// },
|
||||
i18n: true,
|
||||
searchToggle: {
|
||||
enabled: true
|
||||
}
|
||||
|
||||
22
document/components/docs/LocalizedLink.tsx
Normal file
22
document/components/docs/LocalizedLink.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useCurrentLang, useLocalizedPath } from '@/lib/localized-navigation';
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
/**
|
||||
* Localized Link component that automatically adds language prefix
|
||||
* Use this in MDX or React components for internal links
|
||||
*/
|
||||
export function LocalizedLink({ href, ...props }: ComponentProps<typeof Link>) {
|
||||
const lang = useCurrentLang();
|
||||
const localizedPath = useLocalizedPath;
|
||||
|
||||
// Only localize internal links (starting with /)
|
||||
const finalHref =
|
||||
typeof href === 'string' && href.startsWith('/') && !href.startsWith(`/${lang}`)
|
||||
? localizedPath(href)
|
||||
: href;
|
||||
|
||||
return <Link href={finalHref} {...props} />;
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useLocalizedRouter } from '@/lib/localized-navigation';
|
||||
|
||||
interface RedirectProps {
|
||||
to: string;
|
||||
}
|
||||
|
||||
export function Redirect({ to }: RedirectProps) {
|
||||
const router = useRouter();
|
||||
const router = useLocalizedRouter();
|
||||
|
||||
useEffect(() => {
|
||||
router.push(to);
|
||||
|
||||
@@ -1,71 +1,19 @@
|
||||
'use client';
|
||||
import { useEffect } from 'react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
|
||||
const exactMap: Record<string, string> = {
|
||||
'/docs': '/docs/introduction',
|
||||
'/docs/intro': '/docs/introduction',
|
||||
'/docs/guide/dashboard/workflow/coreferenceresolution':
|
||||
'/docs/introduction/guide/dashboard/workflow/coreferenceResolution',
|
||||
'/docs/guide/admin/sso_dingtalk':
|
||||
'/docs/introduction/guide/admin/sso#/docs/introduction/guide/admin/sso#钉钉',
|
||||
'/docs/guide/knowledge_base/rag': '/docs/introduction/guide/knowledge_base/RAG',
|
||||
'/docs/commercial/intro/': '/docs/introduction/commercial',
|
||||
'/docs/upgrading/intro/': '/docs/upgrading',
|
||||
'/docs/introduction/shopping_cart/intro/': '/docs/introduction/commercial'
|
||||
};
|
||||
|
||||
const prefixMap: Record<string, string> = {
|
||||
'/docs/development': '/docs/introduction/development',
|
||||
'/docs/FAQ': '/docs/faq',
|
||||
'/docs/guide': '/docs/introduction/guide',
|
||||
'/docs/shopping_cart': '/docs/introduction/shopping_cart',
|
||||
'/docs/agreement': '/docs/protocol',
|
||||
'/docs/introduction/development/openapi': '/docs/openapi'
|
||||
};
|
||||
|
||||
const fallbackRedirect = '/docs/introduction';
|
||||
import { useCurrentLang } from '@/lib/localized-navigation';
|
||||
import { getLocalizedPath } from '@/lib/i18n';
|
||||
|
||||
/**
|
||||
* Fallback for pages not found
|
||||
* Redirects to introduction page to avoid 404
|
||||
*/
|
||||
export default function NotFound() {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const lang = useCurrentLang();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (exactMap[pathname]) {
|
||||
window.location.replace(exactMap[pathname]);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [oldPrefix, newPrefix] of Object.entries(prefixMap)) {
|
||||
if (pathname.startsWith(oldPrefix)) {
|
||||
const rest = pathname.slice(oldPrefix.length);
|
||||
window.location.replace(newPrefix + rest);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const basePath = pathname.replace(/\/$/, '');
|
||||
const res = await fetch(`/api/meta?path=${basePath}`);
|
||||
console.log('res', res);
|
||||
|
||||
if (!res.ok) throw new Error('meta API not found');
|
||||
|
||||
const validPage = await res.json();
|
||||
|
||||
if (validPage) {
|
||||
console.log('validPage', validPage);
|
||||
window.location.replace(validPage);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('meta.json fallback failed:', e);
|
||||
}
|
||||
|
||||
window.location.replace(fallbackRedirect);
|
||||
})();
|
||||
}, [pathname, router]);
|
||||
// Redirect to introduction page
|
||||
window.location.replace(getLocalizedPath('/docs/introduction', lang));
|
||||
}, [lang]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ FastGPT 使用了 one-api 项目来管理模型池,其可以兼容 OpenAI 、A
|
||||
|
||||
务必先配置至少一组模型,否则系统无法正常使用。
|
||||
|
||||
[点击查看模型配置教程](/docs/introduction/development/modelConfig/intro/)
|
||||
[点击查看模型配置教程](./modelConfig/intro/)
|
||||
|
||||
## 收费
|
||||
|
||||
@@ -110,13 +110,13 @@ FastGPT 商业版共包含了2个应用(fastgpt, fastgpt-plus)和2个数据
|
||||
|
||||
### 如何更新/升级 FastGPT
|
||||
|
||||
[升级脚本文档](/docs/introduction/development/upgrading/)先看下文档,看下需要升级哪个版本。注意,不要跨版本升级!!!!!
|
||||
[升级脚本文档](./upgrading/)先看下文档,看下需要升级哪个版本。注意,不要跨版本升级!!!!!
|
||||
|
||||
例如,目前是4.5 版本,要升级到4.5.1,就先把镜像版本改成v4.5.1,执行一下升级脚本,等待完成后再继续升级。如果目标版本不需要执行初始化,则可以跳过。
|
||||
|
||||
升级步骤:
|
||||
|
||||
1. 查看[更新文档](/docs/introduction/development/upgrading/index/),确认要升级的版本,避免跨版本升级。
|
||||
1. 查看[更新文档](./upgrading/index/),确认要升级的版本,避免跨版本升级。
|
||||
2. 打开 sealos 的应用管理
|
||||
3. 有2个应用 fastgpt , fastgpt-pro
|
||||
4. 点击对应应用右边3个点,变更。或者点详情后右上角的变更。
|
||||
@@ -145,7 +145,7 @@ FastGPT 商业版共包含了2个应用(fastgpt, fastgpt-plus)和2个数据
|
||||
|
||||

|
||||
|
||||
[配置文件参考](/docs/introduction/development/configuration/)
|
||||
[配置文件参考](./configuration/)
|
||||
|
||||
### 修改站点名称以及 favicon
|
||||
|
||||
|
||||
82
document/content/docs/introduction/index.en.mdx
Normal file
82
document/content/docs/introduction/index.en.mdx
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: Quick Introduction to FastGPT
|
||||
description: FastGPT's Capabilities and Advantages
|
||||
---
|
||||
|
||||
import { Alert } from '@/components/docs/Alert';
|
||||
|
||||
FastGPT is a knowledge base Q&A system based on LLM (Large Language Models), perfectly combining intelligent dialogue with visual orchestration to make AI application development simple and natural. Whether you are a developer or a business user, you can easily create your own AI applications.
|
||||
|
||||
<Alert icon="🤖" context="success">
|
||||
Quick Start
|
||||
- International: [https://fastgpt.io](https://fastgpt.io)
|
||||
- China Mainland: [https://fastgpt.cn](https://fastgpt.cn)
|
||||
</Alert>
|
||||
|
||||
| | |
|
||||
| --------------------- | --------------------------------- |
|
||||
|  |  |
|
||||
|
||||
# FastGPT's Advantages
|
||||
## 1. Simple and Flexible, Like Building Blocks 🧱
|
||||
As simple and fun as building with LEGO, FastGPT provides rich functional modules. You can build personalized AI applications through simple drag-and-drop, and implement complex business processes with zero code.
|
||||
## 2. Make Data Smarter 🧠
|
||||
FastGPT provides a complete data intelligence solution, from data import, preprocessing to knowledge matching, and then to intelligent Q&A, with full-process automation. Combined with visual workflow design, you can easily create professional-grade AI applications.
|
||||
## 3. Open Source and Easy to Integrate 🔗
|
||||
FastGPT is open source under the Apache 2.0 license and supports secondary development. It can be quickly integrated through standard APIs without modifying the source code. It supports mainstream models such as ChatGPT, Claude, DeepSeek, and Wenxin Yiyan, with continuous iteration and optimization to maintain product vitality.
|
||||
|
||||
---
|
||||
|
||||
# What Can FastGPT Do
|
||||
## 1. Comprehensive Knowledge Base
|
||||
You can easily import various documents and data, which will be automatically processed for knowledge structuring. It also features intelligent Q&A with multi-turn context understanding, providing users with a continuously optimized knowledge base management experience.
|
||||

|
||||
|
||||
## 2. Visual Workflow
|
||||
FastGPT's intuitive drag-and-drop interface design allows you to build complex business processes with zero code. It has rich functional node components that can handle various business needs, with flexible process orchestration capabilities to customize business processes on demand.
|
||||

|
||||
|
||||
## 3. Intelligent Data Parsing
|
||||
FastGPT's knowledge base system is extremely flexible in processing imported data. It can intelligently handle the complex structure of PDF documents, preserve images, tables, and LaTeX formulas, automatically recognize scanned files, and structure content into clear Markdown format. It also supports automatic image annotation and indexing, making visual content understandable and searchable, ensuring that knowledge is presented and applied completely and accurately in AI Q&A.
|
||||
|
||||

|
||||
|
||||
## 4. Workflow Orchestration
|
||||
Based on Flow module workflow orchestration, you can design more complex Q&A processes. For example, querying databases, checking inventory, booking laboratories, etc.
|
||||
|
||||

|
||||
|
||||
## 5. Powerful API Integration
|
||||
FastGPT is fully aligned with OpenAI's official interface, supporting one-click integration with platforms such as Enterprise WeChat, Official Accounts, Feishu, and DingTalk, allowing AI capabilities to easily integrate into your business scenarios.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
# Core Features
|
||||
|
||||
- Out-of-the-box knowledge base system
|
||||
- Visual low-code workflow orchestration
|
||||
- Support for mainstream large models
|
||||
- Simple and easy-to-use API interface
|
||||
- Flexible data processing capabilities
|
||||
|
||||
---
|
||||
|
||||
# Knowledge Base Core Process Diagram
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
FastGPT is an open source project driven by users and contributors. If you have questions or suggestions about the product, you can seek support in the following ways. Our team and community will do our best to help you.
|
||||
|
||||
- 📱 Scan to join Feishu group 👇
|
||||
|
||||
<img
|
||||
width="400px"
|
||||
src="https://oss.laf.run/otnvvf-imgs/fastgpt-feishu1.png"
|
||||
className="medium-zoom-image"
|
||||
/>
|
||||
|
||||
- 🐞 Please submit any FastGPT bugs, issues, and requirements to [GitHub Issue](https://github.com/labring/fastgpt/issues/new/choose).
|
||||
@@ -3,6 +3,7 @@ title: FastGPT Toc
|
||||
description: FastGPT Toc
|
||||
---
|
||||
|
||||
- [/en/docs/introduction/index](/en/docs/introduction/index)
|
||||
- [/en/docs/protocol/open-source](/en/docs/protocol/open-source)
|
||||
- [/en/docs/protocol/privacy](/en/docs/protocol/privacy)
|
||||
- [/en/docs/protocol/terms](/en/docs/protocol/terms)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"document/content/docs/faq/index.mdx": "2025-08-02T19:38:37+08:00",
|
||||
"document/content/docs/faq/other.mdx": "2025-08-04T22:07:52+08:00",
|
||||
"document/content/docs/faq/points_consumption.mdx": "2025-08-02T19:38:37+08:00",
|
||||
"document/content/docs/introduction/cloud.mdx": "2026-02-26T00:22:01+08:00",
|
||||
"document/content/docs/introduction/cloud.mdx": "2026-02-26T00:26:52+08:00",
|
||||
"document/content/docs/introduction/commercial.mdx": "2025-09-21T23:09:46+08:00",
|
||||
"document/content/docs/introduction/development/configuration.mdx": "2025-08-05T23:20:39+08:00",
|
||||
"document/content/docs/introduction/development/custom-models/bge-rerank.mdx": "2025-07-23T21:35:03+08:00",
|
||||
@@ -87,7 +87,8 @@
|
||||
"document/content/docs/introduction/guide/team_permissions/customDomain.mdx": "2025-12-10T20:07:05+08:00",
|
||||
"document/content/docs/introduction/guide/team_permissions/invitation_link.mdx": "2025-07-23T21:35:03+08:00",
|
||||
"document/content/docs/introduction/guide/team_permissions/team_roles_permissions.mdx": "2025-07-23T21:35:03+08:00",
|
||||
"document/content/docs/introduction/index.mdx": "2025-09-29T11:34:11+08:00",
|
||||
"document/content/docs/introduction/index.en.mdx": "2026-02-26T00:54:15+08:00",
|
||||
"document/content/docs/introduction/index.mdx": "2026-02-26T00:26:52+08:00",
|
||||
"document/content/docs/openapi/app.mdx": "2026-02-12T18:45:30+08:00",
|
||||
"document/content/docs/openapi/chat.mdx": "2026-02-12T18:45:30+08:00",
|
||||
"document/content/docs/openapi/dataset.mdx": "2026-02-12T18:45:30+08:00",
|
||||
@@ -101,11 +102,11 @@
|
||||
"document/content/docs/protocol/privacy.mdx": "2025-12-15T23:36:54+08:00",
|
||||
"document/content/docs/protocol/terms.en.mdx": "2025-12-15T23:36:54+08:00",
|
||||
"document/content/docs/protocol/terms.mdx": "2025-12-15T23:36:54+08:00",
|
||||
"document/content/docs/toc.en.mdx": "2026-02-12T18:02:02+08:00",
|
||||
"document/content/docs/toc.en.mdx": "2026-02-26T00:54:15+08:00",
|
||||
"document/content/docs/toc.mdx": "2026-02-24T13:48:31+08:00",
|
||||
"document/content/docs/upgrading/4-10/4100.mdx": "2025-08-02T19:38:37+08:00",
|
||||
"document/content/docs/upgrading/4-10/4101.mdx": "2025-09-08T20:07:20+08:00",
|
||||
"document/content/docs/upgrading/4-11/4110.mdx": "2025-08-05T23:20:39+08:00",
|
||||
"document/content/docs/upgrading/4-11/4110.mdx": "2026-02-26T00:26:52+08:00",
|
||||
"document/content/docs/upgrading/4-11/4111.mdx": "2025-08-07T22:49:09+08:00",
|
||||
"document/content/docs/upgrading/4-12/4120.mdx": "2025-09-07T14:41:48+08:00",
|
||||
"document/content/docs/upgrading/4-12/4121.mdx": "2025-09-07T14:41:48+08:00",
|
||||
@@ -202,7 +203,7 @@
|
||||
"document/content/docs/use-cases/app-cases/translate-subtitle-using-gpt.mdx": "2025-07-23T21:35:03+08:00",
|
||||
"document/content/docs/use-cases/external-integration/dingtalk.mdx": "2025-07-23T21:35:03+08:00",
|
||||
"document/content/docs/use-cases/external-integration/feishu.mdx": "2025-07-24T14:23:04+08:00",
|
||||
"document/content/docs/use-cases/external-integration/official_account.mdx": "2026-02-12T18:45:30+08:00",
|
||||
"document/content/docs/use-cases/external-integration/official_account.mdx": "2026-02-26T00:26:52+08:00",
|
||||
"document/content/docs/use-cases/external-integration/openapi.mdx": "2026-02-12T18:45:30+08:00",
|
||||
"document/content/docs/use-cases/external-integration/wecom.mdx": "2025-12-10T20:07:05+08:00",
|
||||
"document/content/docs/use-cases/index.mdx": "2025-07-24T14:23:04+08:00"
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { I18nConfig } from 'fumadocs-core/i18n';
|
||||
export const i18n: I18nConfig = {
|
||||
defaultLanguage: 'zh-CN',
|
||||
languages: ['zh-CN', 'en'],
|
||||
hideLocale: 'default-locale'
|
||||
hideLocale: 'never'
|
||||
};
|
||||
|
||||
export async function getTranslations(locale: string) {
|
||||
@@ -29,3 +29,35 @@ export function t(key: string, locale?: string) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get localized URL path based on i18n configuration
|
||||
* @param path - The base path (e.g., '/docs/introduction')
|
||||
* @param lang - The language code
|
||||
* @returns Localized path with language prefix if needed
|
||||
*/
|
||||
export function getLocalizedPath(path: string, lang: string): string {
|
||||
// If hideLocale is 'never', always add language prefix
|
||||
if (i18n.hideLocale === 'never') {
|
||||
return `/${lang}${path}`;
|
||||
}
|
||||
|
||||
// If hideLocale is 'always', never add language prefix
|
||||
if (i18n.hideLocale === 'always') {
|
||||
return path;
|
||||
}
|
||||
|
||||
// If hideLocale is 'default-locale', only add prefix for non-default languages
|
||||
if (i18n.hideLocale === 'default-locale') {
|
||||
return lang === i18n.defaultLanguage ? path : `/${lang}${path}`;
|
||||
}
|
||||
|
||||
// Fallback: no prefix
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-side redirect with automatic language prefix
|
||||
* Import from next/navigation and use this wrapper
|
||||
*/
|
||||
export { redirect } from 'next/navigation';
|
||||
|
||||
53
document/lib/localized-navigation.ts
Normal file
53
document/lib/localized-navigation.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { i18n, getLocalizedPath } from './i18n';
|
||||
|
||||
/**
|
||||
* Get current language from pathname
|
||||
*/
|
||||
export function useCurrentLang(): string {
|
||||
const pathname = usePathname();
|
||||
|
||||
// Extract language from pathname (e.g., /zh-CN/docs/... -> zh-CN)
|
||||
const segments = pathname.split('/').filter(Boolean);
|
||||
const firstSegment = segments[0];
|
||||
|
||||
// Check if first segment is a valid language
|
||||
if (i18n.languages.includes(firstSegment)) {
|
||||
return firstSegment;
|
||||
}
|
||||
|
||||
return i18n.defaultLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get localized path for current language
|
||||
*/
|
||||
export function useLocalizedPath(path: string): string {
|
||||
const lang = useCurrentLang();
|
||||
return getLocalizedPath(path, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Router with automatic language prefix handling
|
||||
*/
|
||||
export function useLocalizedRouter() {
|
||||
const router = useRouter();
|
||||
const lang = useCurrentLang();
|
||||
|
||||
return {
|
||||
push: (path: string) => {
|
||||
router.push(getLocalizedPath(path, lang));
|
||||
},
|
||||
replace: (path: string) => {
|
||||
router.replace(getLocalizedPath(path, lang));
|
||||
},
|
||||
prefetch: (path: string) => {
|
||||
router.prefetch(getLocalizedPath(path, lang));
|
||||
},
|
||||
back: () => router.back(),
|
||||
forward: () => router.forward(),
|
||||
refresh: () => router.refresh(),
|
||||
};
|
||||
}
|
||||
@@ -3,12 +3,14 @@ import type { MDXComponents } from 'mdx/types';
|
||||
import { ImageZoom } from 'fumadocs-ui/components/image-zoom';
|
||||
import * as TabsComponents from 'fumadocs-ui/components/tabs';
|
||||
import { TypeTable } from 'fumadocs-ui/components/type-table';
|
||||
import { LocalizedLink } from '@/components/docs/LocalizedLink';
|
||||
|
||||
// use this function to get MDX components, you will need it for rendering MDX
|
||||
export function getMDXComponents(components?: MDXComponents): MDXComponents {
|
||||
return {
|
||||
...defaultMdxComponents,
|
||||
img: (props) => <ImageZoom {...(props as any)} />,
|
||||
a: (props) => <LocalizedLink {...(props as any)} />,
|
||||
...TabsComponents,
|
||||
...components,
|
||||
TypeTable
|
||||
|
||||
@@ -1,9 +1,67 @@
|
||||
import { createI18nMiddleware } from 'fumadocs-core/i18n';
|
||||
import { i18n } from '@/lib/i18n';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export default createI18nMiddleware(i18n);
|
||||
// Old path redirects mapping
|
||||
const exactMap: Record<string, string> = {
|
||||
'/docs': '/docs/introduction',
|
||||
'/docs/intro': '/docs/introduction',
|
||||
'/docs/guide/dashboard/workflow/coreferenceresolution':
|
||||
'/docs/introduction/guide/dashboard/workflow/coreferenceResolution',
|
||||
'/docs/guide/admin/sso_dingtalk':
|
||||
'/docs/introduction/guide/admin/sso#/docs/introduction/guide/admin/sso#钉钉',
|
||||
'/docs/guide/knowledge_base/rag': '/docs/introduction/guide/knowledge_base/RAG',
|
||||
'/docs/commercial/intro/': '/docs/introduction/commercial',
|
||||
'/docs/upgrading/intro/': '/docs/upgrading',
|
||||
'/docs/introduction/shopping_cart/intro/': '/docs/introduction/commercial'
|
||||
};
|
||||
|
||||
const prefixMap: Record<string, string> = {
|
||||
'/docs/development': '/docs/introduction/development',
|
||||
'/docs/FAQ': '/docs/faq',
|
||||
'/docs/guide': '/docs/introduction/guide',
|
||||
'/docs/shopping_cart': '/docs/introduction/shopping_cart',
|
||||
'/docs/agreement': '/docs/protocol',
|
||||
'/docs/introduction/development/openapi': '/docs/openapi'
|
||||
};
|
||||
|
||||
const i18nMiddleware = createI18nMiddleware(i18n);
|
||||
|
||||
export default function middleware(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
|
||||
// Extract language from pathname
|
||||
let lang = i18n.defaultLanguage;
|
||||
let pathWithoutLang = pathname;
|
||||
|
||||
for (const language of i18n.languages) {
|
||||
if (pathname.startsWith(`/${language}`)) {
|
||||
lang = language;
|
||||
pathWithoutLang = pathname.slice(`/${language}`.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check exact match redirects
|
||||
if (exactMap[pathWithoutLang]) {
|
||||
const newUrl = new URL(`/${lang}${exactMap[pathWithoutLang]}`, request.url);
|
||||
return NextResponse.redirect(newUrl, 301);
|
||||
}
|
||||
|
||||
// Check prefix match redirects
|
||||
for (const [oldPrefix, newPrefix] of Object.entries(prefixMap)) {
|
||||
if (pathWithoutLang.startsWith(oldPrefix)) {
|
||||
const rest = pathWithoutLang.slice(oldPrefix.length);
|
||||
const newUrl = new URL(`/${lang}${newPrefix}${rest}`, request.url);
|
||||
return NextResponse.redirect(newUrl, 301);
|
||||
}
|
||||
}
|
||||
|
||||
// Continue with i18n middleware
|
||||
// @ts-expect-error - Fumadocs middleware signature mismatch
|
||||
return i18nMiddleware(request);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
// matcher: ['/((?!api|_next/static|_next/image|favicon.ico|.*\\.svg|.*\\.png).*)']
|
||||
matcher: ['/((?!api|_next/static|_next/image|favicon.ico|.*\\.svg|.*\\.png|deploy/.*).*)']
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user