UI: Deprecate I18nProvider in favour of <RootProvider /> i18n prop

This commit is contained in:
Fuma Nama
2025-04-03 23:31:28 +08:00
parent 293178f763
commit 5e4e9ec541
5 changed files with 178 additions and 105 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-ui': patch
---
Deprecate I18nProvider in favour of `<RootProvider />` `i18n` prop
+18 -19
View File
@@ -2,7 +2,6 @@ import 'fumadocs-ui/style.css';
import { RootProvider } from 'fumadocs-ui/provider';
import { Inter } from 'next/font/google';
import type { ReactNode } from 'react';
import { I18nProvider } from 'fumadocs-ui/i18n';
const inter = Inter({
subsets: ['latin'],
@@ -25,20 +24,20 @@ export default async function Layout({
minHeight: '100vh',
}}
>
<I18nProvider
locale={lang}
locales={[
{
name: 'English',
locale: 'en',
},
{
name: 'Chinese',
locale: 'cn',
},
]}
translations={
{
<RootProvider
i18n={{
locale: lang,
locales: [
{
name: 'English',
locale: 'en',
},
{
name: 'Chinese',
locale: 'cn',
},
],
translations: {
cn: {
toc: '目錄',
search: '搜尋文檔',
@@ -48,11 +47,11 @@ export default async function Layout({
nextPage: '下一頁',
chooseLanguage: '選擇語言',
},
}[lang]
}
}[lang],
}}
>
<RootProvider>{children}</RootProvider>
</I18nProvider>
{children}
</RootProvider>
</body>
</html>
);
+1 -79
View File
@@ -1,7 +1,5 @@
'use client';
import { useEffectEvent } from 'fumadocs-core/utils/use-effect-event';
import { createContext, type ReactNode, useContext, useMemo } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import { createContext, useContext } from 'react';
export interface Translations {
search: string;
@@ -56,79 +54,3 @@ export function I18nLabel(props: { label: keyof Translations }): string {
export function useI18n(): I18nContextType {
return useContext(I18nContext);
}
export interface I18nProviderProps {
/**
* Current locale
*/
locale: string;
/**
* Handle changes to the locale, redirect user when not specified.
*/
onLocaleChange?: (v: string) => void;
/**
* Translations of current locale
*/
translations?: Partial<Translations>;
/**
* Available languages
*/
locales?: LocaleItem[];
children?: ReactNode;
}
export function I18nProvider({
locales = [],
locale,
onChange: _onChange,
onLocaleChange = _onChange,
...props
}: I18nProviderProps & {
// TODO: remove next major
/**
* @deprecated use `onLocaleChange` instead
*/
onChange?: I18nProviderProps['onLocaleChange'];
}) {
const router = useRouter();
const pathname = usePathname();
const onChange = useEffectEvent((value: string) => {
if (onLocaleChange) {
return onLocaleChange(value);
}
const segments = pathname.split('/').filter((v) => v.length > 0);
// If locale prefix hidden
if (segments[0] !== locale) {
segments.unshift(value);
} else {
segments[0] = value;
}
router.push(`/${segments.join('/')}`);
router.refresh();
});
return (
<I18nContext.Provider
value={useMemo(
() => ({
locale,
locales,
text: {
...defaultTranslations,
...props.translations,
},
onChange,
}),
[locale, locales, onChange, props.translations],
)}
>
{props.children}
</I18nContext.Provider>
);
}
+64 -6
View File
@@ -1,6 +1,64 @@
export {
defaultTranslations,
type Translations,
I18nProvider,
type I18nProviderProps,
} from './contexts/i18n';
'use client';
import { usePathname, useRouter } from 'next/navigation';
import { useEffectEvent } from 'fumadocs-core/utils/use-effect-event';
import { useMemo } from 'react';
import { defaultTranslations, I18nContext } from '@/contexts/i18n';
import type { I18nProviderProps } from './provider/base';
export type { I18nProviderProps };
export { defaultTranslations, type Translations } from './contexts/i18n';
// TODO: remove next major
/**
* @deprecated legacy I18n Provider, use `<RootProvider i18n={...} />` instead
*/
export function I18nProvider({
locales = [],
locale,
onChange: _onChange,
onLocaleChange = _onChange,
...props
}: I18nProviderProps & {
/**
* @deprecated use `onLocaleChange` instead
*/
onChange?: I18nProviderProps['onLocaleChange'];
}) {
const router = useRouter();
const pathname = usePathname();
const onChange = useEffectEvent((value: string) => {
if (onLocaleChange) {
return onLocaleChange(value);
}
const segments = pathname.split('/').filter((v) => v.length > 0);
// If locale prefix hidden
if (segments[0] !== locale) {
segments.unshift(value);
} else {
segments[0] = value;
}
router.push(`/${segments.join('/')}`);
router.refresh();
});
return (
<I18nContext.Provider
value={useMemo(
() => ({
locale,
locales,
text: {
...defaultTranslations,
...props.translations,
},
onChange,
}),
[locale, locales, onChange, props.translations],
)}
>
{props.children}
</I18nContext.Provider>
);
}
+90 -1
View File
@@ -1,11 +1,24 @@
'use client';
import { ThemeProvider } from 'next-themes';
import { type ComponentPropsWithoutRef, lazy, type ReactNode } from 'react';
import {
type ComponentPropsWithoutRef,
lazy,
type ReactNode,
useMemo,
} from 'react';
import { DirectionProvider } from '@radix-ui/react-direction';
import type { DefaultSearchDialogProps } from '@/components/dialog/search-default';
import { SidebarProvider } from '@/contexts/sidebar';
import { SearchProvider, type SearchProviderProps } from '@/contexts/search';
import { useEffectEvent } from 'fumadocs-core/utils/use-effect-event';
import {
defaultTranslations,
I18nContext,
type LocaleItem,
type Translations,
} from '@/contexts/i18n';
import { usePathname, useRouter } from 'fumadocs-core/framework';
interface SearchOptions
extends Omit<SearchProviderProps, 'options' | 'children'> {
@@ -41,6 +54,32 @@ export interface RootProviderProps {
enabled?: boolean;
};
i18n?: Omit<I18nProviderProps, 'children'>;
children?: ReactNode;
}
export interface I18nProviderProps {
/**
* Current locale
*/
locale: string;
/**
* Handle changes to the locale, redirect user when not specified.
*/
onLocaleChange?: (v: string) => void;
/**
* Translations of current locale
*/
translations?: Partial<Translations>;
/**
* Available languages
*/
locales?: LocaleItem[];
children?: ReactNode;
}
@@ -53,9 +92,14 @@ export function RootProvider({
dir = 'ltr',
theme = {},
search,
i18n,
}: RootProviderProps) {
let body = children;
if (i18n) {
body = <I18nProvider {...i18n}>{body}</I18nProvider>;
}
if (search?.enabled !== false)
body = (
<SearchProvider SearchDialog={DefaultSearchDialog} {...search}>
@@ -82,3 +126,48 @@ export function RootProvider({
</DirectionProvider>
);
}
function I18nProvider({
locales = [],
locale,
onLocaleChange,
...props
}: I18nProviderProps) {
const router = useRouter();
const pathname = usePathname();
const onChange = useEffectEvent((value: string) => {
if (onLocaleChange) {
return onLocaleChange(value);
}
const segments = pathname.split('/').filter((v) => v.length > 0);
// If locale prefix hidden
if (segments[0] !== locale) {
segments.unshift(value);
} else {
segments[0] = value;
}
router.push(`/${segments.join('/')}`);
router.refresh();
});
return (
<I18nContext.Provider
value={useMemo(
() => ({
locale,
locales,
text: {
...defaultTranslations,
...props.translations,
},
onChange,
}),
[locale, locales, onChange, props.translations],
)}
>
{props.children}
</I18nContext.Provider>
);
}