Dify实现了一个完整的国际化解决方案,支持多语言切换,包括服务端渲染(SSR)和客户端渲染(CSR)的完整支持。基于 i18next
和 react-i18next
构建,提供了类型安全的国际化体验。
项目结构
i18n/
├── README.md # 本文档
├── index.ts # 主要导出文件,客户端语言管理
├── i18next-config.ts # i18next 配置和资源加载
├── language.ts # 语言相关工具函数
├── server.ts # 服务端语言检测和翻译
├── languages.json # 支持的语言配置
├── en-US/ # 英文翻译文件
│ ├── common.ts # 通用翻译
│ └── apps.ts # 应用相关翻译
├── zh-Hans/ # 简体中文翻译文件
│ ├── common.ts
│ └── apps.ts
└── zh-Hant/ # 繁体中文翻译文件
├── common.ts
└── apps.ts
核心文件详解
1. i18n/index.ts
- 主要导出文件
作用: 提供客户端语言管理的核心功能
import Cookies from 'js-cookie'
import { changeLanguage } from '@/i18n/i18next-config'
import { LOCALE_COOKIE_NAME } from '@/config'
import { LanguagesSupported } from '@/i18n/language'
export const i18n = {
defaultLocale: 'en-US',
locales: LanguagesSupported,
} as const
export type Locale = typeof i18n['locales'][number]
// 设置客户端语言
export const setLocaleOnClient = (locale: Locale, reloadPage = true) => {
Cookies.set(LOCALE_COOKIE_NAME, locale)
changeLanguage(locale)
reloadPage && location.reload()
}
// 获取客户端语言
export const getLocaleOnClient = (): Locale => {
return Cookies.get(LOCALE_COOKIE_NAME) as Locale || i18n.defaultLocale
}
// 渲染国际化对象
export const renderI18nObject = (obj: Record<string, string>, language: string) => {
if (!obj) return ''
if (obj?.[language]) return obj[language]
if (obj?.en_US) return obj.en_US
return Object.values(obj)[0]
}
2. i18n/i18next-config.ts
- i18next 配置
作用: 配置 i18next 实例,加载语言资源
'use client'
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import { LanguagesSupported } from '@/i18n/language'
// 静默加载语言资源
const requireSilent = (lang: string) => {
let res
try {
res = require(`./${lang}/common`).default
}
catch {
res = require('./en-US/common').default
}
return res
}
// 加载语言资源
const loadLangResources = (lang: string) => ({
translation: {
app: require(`./${lang}/apps`).default,
common: require(`./${lang}/common`).default,
},
})
// 自动生成资源对象
export const resources = LanguagesSupported.reduce<Resource>((acc, lang) => {
acc[lang] = loadLangResources(lang)
return acc
}, {})
// 初始化 i18next
i18n.use(initReactI18next)
.init({
lng: undefined,
fallbackLng: 'en-US',
resources,
})
export const changeLanguage = i18n.changeLanguage
export default i18n
3. i18n/language.ts
- 语言工具函数
作用: 提供语言相关的工具函数和配置
import data from './languages.json'
export type Item = {
value: number | string
name: string
example: string
}
export type I18nText = {
'en-US': string
'zh-Hans': string
}
export const languages = data.languages
// 获取支持的语言列表
export const LanguagesSupported = languages.filter(item => item.supported).map(item => item.value)
// 获取语言代码
export const getLanguage = (locale: string) => {
if (['zh-Hans', 'ja-JP'].includes(locale))
return locale.replace('-', '_')
return LanguagesSupported[0].replace('-', '_')
}
// 获取文档语言
export const getDocLanguage = (locale: string) => {
const DOC_LANGUAGE: Record<string, string> = {
'zh-Hans': 'zh-hans',
'ja-JP': 'ja-jp',
'en-US': 'en',
}
return DOC_LANGUAGE[locale] || 'en'
}
// 获取定价页面语言
export const getPricingPageLanguage = (locale: string) => {
const PRICING_PAGE_LANGUAGE: Record<string, string> = {
'ja-JP': 'jp',
}
return PRICING_PAGE_LANGUAGE[locale] || ''
}
4. i18n/server.ts
- 服务端语言处理
作用: 处理服务端语言检测和翻译
import { cookies, headers } from 'next/headers'
import Negotiator from 'negotiator'
import { match } from '@formatjs/intl-localematcher'
import { createInstance } from 'i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
import { initReactI18next } from 'react-i18next/initReactI18next'
import { i18n } from '.'
import type { Locale } from '.'
// 初始化 i18next 实例
const initI18next = async (lng: Locale, ns: string) => {
const i18nInstance = createInstance()
await i18nInstance
.use(initReactI18next)
.use(resourcesToBackend((language: string, namespace: string) =>
import(`./${language}/${namespace}.ts`)))
.init({
lng: lng === 'zh-Hans' ? 'zh-Hans' : lng,
ns,
fallbackLng: 'en-US',
})
return i18nInstance
}
// 使用翻译
export async function useTranslation(lng: Locale, ns = '', options: Record<string, any> = {}) {
const i18nextInstance = await initI18next(lng, ns)
return {
t: i18nextInstance.getFixedT(lng, ns, options.keyPrefix),
i18n: i18nextInstance,
}
}
// 获取服务端语言
export const getLocaleOnServer = async (): Promise<Locale> => {
const locales: string[] = [...i18n.locales]
let languages: string[] | undefined
// 1. 从 cookie 获取语言
const localeCookie = (await cookies()).get('locale')
languages = localeCookie?.value ? [localeCookie.value] : []
if (!languages.length) {
// 2. 从 HTTP 头获取语言
const negotiatorHeaders: Record<string, string> = {};
(await headers()).forEach((value, key) => (negotiatorHeaders[key] = value))
languages = new Negotiator({ headers: negotiatorHeaders }).languages()
}
// 验证语言
if (!Array.isArray(languages) || languages.length === 0 ||
!languages.every(lang => typeof lang === 'string' && /^[\w-]+$/.test(lang)))
languages = [i18n.defaultLocale]
// 匹配语言
const matchedLocale = match(languages, locales, i18n.defaultLocale) as Locale
return matchedLocale
}
5. i18n/languages.json
- 语言配置
作用: 定义支持的语言列表
{
"languages": [
{
"value": "en-US",
"name": "English (United States)",
"prompt_name": "English",
"example": "Hello, Dify!",
"supported": true
},
{
"value": "zh-Hans",
"name": "简体中文",
"prompt_name": "Chinese Simplified",
"example": "你好,Dify!",
"supported": true
},
{
"value": "zh-Hant",
"name": "繁體中文",
"prompt_name": "Chinese Traditional",
"example": "你好,Dify!",
"supported": true
}
]
}
React 组件集成
1. app/components/i18n-server.tsx
- 服务端组件
作用: 服务端语言提供者组件
import React from 'react'
import I18N from './i18n'
import { ToastProvider } from './base/toast'
import { getLocaleOnServer } from '@/i18n/server'
export type II18NServerProps = {
children: React.ReactNode
}
const I18NServer = async ({
children,
}: II18NServerProps) => {
const locale = await getLocaleOnServer()
return (
<I18N {...{ locale }}>
<ToastProvider>{children}</ToastProvider>
</I18N>
)
}
export default I18NServer
2. app/components/i18n.tsx
- 客户端组件
作用: 客户端语言提供者组件
'use client'
import type { FC } from 'react'
import React, { useEffect } from 'react'
import I18NContext from '@/context/i18n'
import type { Locale } from '@/i18n'
import { setLocaleOnClient } from '@/i18n'
export type II18nProps = {
locale: Locale
children: React.ReactNode
}
const I18n: FC<II18nProps> = ({
locale,
children,
}) => {
useEffect(() => {
setLocaleOnClient(locale, false)
}, [locale])
return (
<I18NContext.Provider value={{
locale,
i18n: {},
setLocaleOnClient,
}}>
{children}
</I18NContext.Provider>
)
}
export default React.memo(I18n)
3. context/i18n.ts
- React Context
作用: 提供全局语言上下文
import {
createContext,
useContext,
} from 'use-context-selector'
import type { Locale } from '@/i18n'
import { getDocLanguage, getLanguage, getPricingPageLanguage } from '@/i18n/language'
import { noop } from 'lodash-es'
type II18NContext = {
locale: Locale
i18n: Record<string, any>
setLocaleOnClient: (_lang: Locale, _reloadPage?: boolean) => void
}
const I18NContext = createContext<II18NContext>({
locale: 'en-US',
i18n: {},
setLocaleOnClient: noop,
})
export const useI18N = () => useContext(I18NContext)
export const useGetLanguage = () => {
const { locale } = useI18N()
return getLanguage(locale)
}
export const useGetDocLanguage = () => {
const { locale } = useI18N()
return getDocLanguage(locale)
}
export const useGetPricingPageLanguage = () => {
const { locale } = useI18N()
return getPricingPageLanguage(locale)
}
export default I18NContext
翻译文件结构
通用翻译 (common.ts
)
export default {
welcome: 'Welcome',
apps: 'Apps',
home: 'Home',
about: 'About',
contact: 'Contact',
language: 'Language',
english: 'English',
chinese: 'Chinese',
} as const
应用翻译 (apps.ts
)
export default {
title: 'Applications',
description: 'Browse and manage your applications',
features: {
title: 'Features',
list: [
'Easy to use interface',
'Fast performance',
'Secure and reliable',
'24/7 support'
]
},
stats: {
total: 'Total Applications',
active: 'Active',
inactive: 'Inactive'
},
actions: {
create: 'Create New App',
edit: 'Edit',
delete: 'Delete',
view: 'View Details'
}
} as const
使用方法
1. 在页面中使用翻译
// 服务端组件
import { useTranslation } from 'react-i18next'
export default function AppsPage() {
const { t } = useTranslation()
return (
<>
hello {t('app.title')}
</>
)
}
2. 在客户端组件中使用
'use client'
import { useI18N } from '@/context/i18n'
export default function ClientComponent() {
const { locale, setLocaleOnClient } = useI18N()
const handleLanguageChange = (newLocale: string) => {
setLocaleOnClient(newLocale as any)
}
return (
<div>
<p>Current language: {locale}</p>
<button onClick={() => handleLanguageChange('zh-Hans')}>
Switch to Chinese
</button>
</div>
)
}
3. 在布局中集成
// app/layout.tsx
import I18nServer from './components/i18n-server'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<I18nServer>
{children}
</I18nServer>
</body>
</html>
)
}
测试
以下是测试语言的代码,测试切换功能是否生效:
// app/apps/page.tsx
import { LanguageSwitcher } from '@/app/components/language-switcher'
import { useTranslation } from '@/i18n/server'
import { getLocaleOnServer } from '@/i18n/server'
export default async function AppsPage() {
const locale = await getLocaleOnServer()
const { t: tCommon } = await useTranslation(locale, 'common')
const { t: tApps } = await useTranslation(locale, 'apps')
return (
<div>
<LanguageSwitcher />
{tCommon('welcome')} == {tApps('title')}
</div>
)
}
切换语言实现:
// app/components/language-switcher/language-switcher.ts
export { default as LanguageSwitcher } from './language-switcher'
// app/components/language-switcher/index.ts
'use client'
import React, { useState, useRef, useEffect } from 'react'
import { useI18N } from '@/context/i18n'
import { LanguagesSupported, languages } from '@/i18n/language'
import ActionButton from '@/app/components/base/action-button'
import classNames from '@/utils/classnames'
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
import { setLocaleOnClient } from '@/i18n'
import type { Locale } from '@/i18n'
// 国旗emoji映射
const flagEmojis: Record<string, string> = {
'en-US': '🇺🇸',
'zh-Hans': '🇨🇳',
'zh-Hant': '🇹🇼'
}
interface LanguageSwitcherProps {
className?: string
showFlag?: boolean
showNativeName?: boolean
}
const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({
className,
showFlag = true,
showNativeName = true
}) => {
const { locale } = useI18N()
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
// 点击外部关闭下拉菜单
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [])
const switchLanguage = async (locale: string) => {
// 更新 URL 参数
const newSearchParams = new URLSearchParams(searchParams)
if (locale === 'en-US') {
// 英文版:移除hi参数
newSearchParams.delete('hi')
} else if (locale === 'zh-Hans') {
// 简体中文版:设置hi=zh参数
newSearchParams.set('hi', 'zh')
} else if (locale === 'zh-Hant') {
// 繁体中文版:设置hi=zh-tw参数
newSearchParams.set('hi', 'zh-tw')
}
const newUrl = newSearchParams.toString()
? `${pathname}?${newSearchParams.toString()}`
: pathname
// 先更新URL,然后让setLocaleOnClient处理刷新
window.history.replaceState({}, '', newUrl)
// 设置语言并刷新页面
setLocaleOnClient(locale as Locale, true)
setIsOpen(false)
}
// 从languages数据中获取当前语言信息
const currentLanguage = languages.find(lang => lang.value === locale)
return (
<div className={classNames('relative', className)} ref={dropdownRef}>
{/* 当前语言显示 */}
<ActionButton
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
{showFlag && <span className="text-lg">{flagEmojis[locale]}</span>}
<span>{showNativeName ? currentLanguage?.name : currentLanguage?.prompt_name}</span>
<svg
className={classNames(
'w-4 h-4 transition-transform duration-200',
isOpen ? 'rotate-180' : ''
)}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</ActionButton>
{/* 下拉菜单 */}
{isOpen && (
<div className="absolute left-0 z-50 w-48 mt-1 bg-white border border-gray-200 rounded-md shadow-lg">
<div className="py-1">
{LanguagesSupported.map((lang) => {
const langData = languages.find(l => l.value === lang)
const isActive = locale === lang
return (
<button
key={lang}
onClick={() => switchLanguage(lang)}
className={classNames(
'flex items-center w-full px-4 py-2 text-sm text-left transition-colors',
'hover:bg-gray-100 focus:bg-gray-100 focus:outline-none',
isActive ? 'bg-blue-50 text-blue-700' : 'text-gray-700'
)}
>
{showFlag && <span className="mr-3 text-lg">{flagEmojis[lang]}</span>}
<div className="flex flex-col">
<span className="font-medium">
{showNativeName ? langData?.name : langData?.prompt_name}
</span>
{showNativeName && (
<span className="text-xs text-gray-500">
{langData?.prompt_name}
</span>
)}
</div>
{isActive && (
<svg
className="w-4 h-4 ml-auto text-blue-600"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
)}
</button>
)
})}
</div>
</div>
)}
</div>
)
}
export default LanguageSwitcher
语言检测优先级
- Cookie: 优先从
locale
cookie 获取用户选择的语言 - HTTP Headers: 从浏览器
Accept-Language
头获取语言偏好 - 默认语言: 如果以上都不可用,使用
en-US
作为默认语言
添加新语言
- 在
i18n/languages.json
中添加新语言配置 - 创建对应的语言文件夹 (如
i18n/ja-JP/
) - 添加翻译文件 (
common.ts
,apps.ts
等) - 更新类型定义
注意事项
- 所有翻译文件都应该使用
as const
确保类型安全 - 服务端和客户端组件需要分别处理
- 语言切换会触发页面刷新以确保一致性
- 支持的语言列表在
languages.json
中统一管理