Core: Introduce new translations API

This commit is contained in:
Fuma Nama
2026-05-20 19:23:26 +08:00
parent a87b3ea293
commit 214d5b0e0d
30 changed files with 302 additions and 117 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'fumadocs-ui': minor
'@fumadocs/base-ui': minor
'fumadocs-core': minor
---
Introduce new translations API
@@ -14,7 +14,7 @@ import {
useRef,
useState,
} from 'react';
import { I18nLabel, useI18n } from '@/contexts/i18n';
import { I18nLabel, useTranslations } from '@/contexts/i18n';
import { cn } from '@/utils/cn';
import { Dialog } from '@base-ui/react/dialog';
import type { HighlightedText, ReactSortedResult } from 'fumadocs-core/search';
@@ -219,7 +219,7 @@ export function SearchDialogHeader(props: ComponentProps<'div'>) {
}
export function SearchDialogInput(props: ComponentProps<'input'>) {
const { text } = useI18n();
const t = useTranslations();
const { search, onSearchChange } = useSearch();
return (
@@ -227,7 +227,7 @@ export function SearchDialogInput(props: ComponentProps<'input'>) {
{...props}
value={search}
onChange={(e) => onSearchChange(e.target.value)}
placeholder={text.search}
placeholder={t.search}
className="w-0 flex-1 bg-transparent text-lg placeholder:text-fd-muted-foreground focus-visible:outline-none"
/>
);
@@ -285,7 +285,7 @@ export function SearchDialogContent({
className,
...props
}: ComponentProps<typeof Dialog.Popup>) {
const { text } = useI18n();
const t = useTranslations();
return (
<Dialog.Portal>
@@ -301,7 +301,7 @@ export function SearchDialogContent({
)
}
>
<Dialog.Title className="hidden">{text.search}</Dialog.Title>
<Dialog.Title className="hidden">{t.search}</Dialog.Title>
{children}
</Dialog.Popup>
</Dialog.Portal>
@@ -12,7 +12,7 @@ import {
import { cn } from '@/utils/cn';
import { useTOCItems } from '.';
import { mergeRefs } from '@/utils/merge-refs';
import { useI18n } from '@/contexts/i18n';
import { useTranslations } from '@/contexts/i18n';
interface ComputedSVG {
width: number;
@@ -143,11 +143,11 @@ export function TOCItems({ ref, className, thumbBox = true, children, ...props }
}
export function TOCEmpty() {
const { text } = useI18n();
const t = useTranslations();
return (
<div className="rounded-lg border bg-fd-card p-3 text-xs text-fd-muted-foreground">
{text.tocNoHeadings}
{t.tocNoHeadings}
</div>
);
}
@@ -1,5 +1,5 @@
'use client';
import { useI18n } from '@/contexts/i18n';
import { useTranslations } from '@/contexts/i18n';
import { cn } from '@/utils/cn';
import { type ComponentProps, useCallback, useEffect, useRef, useState } from 'react';
import { mergeRefs } from '@/utils/merge-refs';
@@ -102,11 +102,11 @@ function TocThumb({ computed }: { computed: ComputedData }) {
}
export function TOCEmpty() {
const { text } = useI18n();
const t = useTranslations();
return (
<div className="rounded-lg border bg-fd-card p-3 text-xs text-fd-muted-foreground">
{text.tocNoHeadings}
{t.tocNoHeadings}
</div>
);
}
+27 -13
View File
@@ -1,8 +1,10 @@
'use client';
import { createContext, type ReactNode, useContext, useMemo, useRef } from 'react';
import { createContext, type ReactNode, use, useMemo, useRef } from 'react';
import { usePathname, useRouter } from 'fumadocs-core/framework';
import { renderTranslation, TranslationObject } from 'fumadocs-core/i18n';
export interface Translations {
export type Translations = {
displayName: string;
search: string;
searchNoResult: string;
@@ -15,10 +17,6 @@ export interface Translations {
previousPage: string;
chooseTheme: string;
editOnGithub: string;
}
export type TranslationsOption = {
[key: string]: string | TranslationsOption;
};
interface LocaleItem {
@@ -27,13 +25,14 @@ interface LocaleItem {
}
interface I18nContextType {
text: Translations & TranslationsOption;
text: Translations & Record<string, string | Record<string, string>>;
locale?: string;
onChange?: (v: string) => void;
locales?: LocaleItem[];
}
export const defaultTranslations: Translations = {
displayName: 'English',
search: 'Search',
searchNoResult: 'No results found',
toc: 'On this page',
@@ -47,16 +46,31 @@ export const defaultTranslations: Translations = {
};
const I18nContext = createContext<I18nContextType>({
text: { ...defaultTranslations },
text: defaultTranslations,
});
export function I18nLabel(props: { label: keyof Translations }): string {
const text = useI18n().text;
return text[props.label];
export function I18nLabel<
Obj extends TranslationObject = Translations,
K extends keyof Obj = keyof Obj,
>({
label,
namespace,
params,
}: {
label: K;
namespace?: string;
params?: Obj[K]['_params'] extends string ? Record<Obj[K]['_params'], string> : never;
}): string {
const t = useTranslations<Obj>(namespace);
return renderTranslation(t[label], params!);
}
export function useI18n(): I18nContextType {
return useContext(I18nContext);
return use(I18nContext);
}
export function useTranslations<Obj extends TranslationObject>(namespace?: string): Obj {
return (namespace ? use(I18nContext).text[namespace] : use(I18nContext).text) as Obj;
}
export interface I18nProviderProps {
@@ -73,7 +87,7 @@ export interface I18nProviderProps {
/**
* Translations of current locale
*/
translations?: TranslationsOption;
translations?: Partial<I18nContextType['text']>;
/**
* Available languages
+39 -6
View File
@@ -1,8 +1,44 @@
import type { I18nProviderProps, Translations } from '@/contexts/i18n';
import type { I18nConfig } from 'fumadocs-core/i18n';
import { defaultTranslations, type I18nProviderProps, type Translations } from '@/contexts/i18n';
import type { I18nConfig, TranslationsAPI } from 'fumadocs-core/i18n';
export { defaultTranslations, type I18nProviderProps, type Translations } from '@/contexts/i18n';
type TranslationsConfig<Languages extends string> = {
[K in Languages]?: Partial<Translations>;
};
export function uiTranslations(): {
namespace: 'ui';
defaultValues: Translations;
} {
return {
namespace: 'ui',
defaultValues: defaultTranslations,
};
}
export function i18nProvider<
Languages extends string,
P extends {
ui: Translations;
},
>(
translations: TranslationsAPI<Languages, P>,
lang: Languages | (string & {}) = translations.config.defaultLanguage,
): I18nProviderProps {
const { ui, ...rest } =
translations.get(lang) ?? translations.get(translations.config.defaultLanguage);
return {
locale: lang,
translations: { ...ui, ...rest },
locales: translations.config.languages.map((locale) => ({
locale,
name: translations.get(locale).ui.displayName ?? locale,
})),
};
}
export interface I18nUIConfig<Languages extends string> extends I18nConfig<Languages> {
/**
* get i18n config for Fumadocs UI `<RootProvider i18n={config} />`.
@@ -10,10 +46,7 @@ export interface I18nUIConfig<Languages extends string> extends I18nConfig<Langu
provider: (locale?: Languages | (string & {})) => I18nProviderProps;
}
type TranslationsConfig<Languages extends string> = {
[K in Languages]?: Partial<Translations> & { displayName?: string };
};
/** @deprecated use the `defineTranslations()` & `uiTranslations()` APIs instead */
export function defineI18nUI<Languages extends string>(
config: I18nConfig<Languages>,
options:
@@ -9,7 +9,7 @@ import {
useState,
} from 'react';
import { cn } from '@/utils/cn';
import { I18nLabel, useI18n } from '@/contexts/i18n';
import { I18nLabel, useTranslations } from '@/contexts/i18n';
import {
TOC,
TOCPopover,
@@ -207,7 +207,7 @@ export function PageLastUpdate({
date: value,
...props
}: Omit<ComponentProps<'p'>, 'children'> & { date: Date }) {
const { text } = useI18n();
const t = useTranslations();
const [date, setDate] = useState('');
useEffect(() => {
@@ -217,7 +217,7 @@ export function PageLastUpdate({
return (
<p {...props} className={cn('text-sm text-fd-muted-foreground', props.className)}>
{text.lastUpdate} {date}
{t.lastUpdate} {date}
</p>
);
}
@@ -1,6 +1,6 @@
'use client';
import { useI18n } from '@/contexts/i18n';
import { useTranslations } from '@/contexts/i18n';
import { cn } from '@/utils/cn';
import { isActive } from '@/utils/urls';
import { useFooterItems } from '@/utils/use-footer-items';
@@ -56,7 +56,7 @@ export function Footer({ items, children, className, ...props }: FooterProps) {
}
function FooterItem({ item, index }: { item: Item; index: 0 | 1 }) {
const { text } = useI18n();
const t = useTranslations();
const Icon = index === 0 ? ChevronLeft : ChevronRight;
return (
@@ -77,7 +77,7 @@ function FooterItem({ item, index }: { item: Item; index: 0 | 1 }) {
<p>{item.name}</p>
</div>
<p className="text-fd-muted-foreground truncate">
{item.description ?? (index === 0 ? text.previousPage : text.nextPage)}
{item.description ?? (index === 0 ? t.previousPage : t.nextPage)}
</p>
</Link>
);
@@ -2,7 +2,7 @@
import * as TocDefault from '@/components/toc/default';
import * as TocClerk from '@/components/toc/clerk';
import * as Base from '@/components/toc';
import { I18nLabel, useI18n } from '@/contexts/i18n';
import { I18nLabel, useTranslations } from '@/contexts/i18n';
import { cn } from '@/utils/cn';
import { ChevronDown, Text } from 'lucide-react';
import {
@@ -193,7 +193,7 @@ export function TOCPopover({
}
function PageTOCPopoverTrigger({ className, ...props }: ComponentProps<'button'>) {
const { text } = useI18n();
const t = useTranslations();
const { open } = use(TocPopoverContext)!;
const items = Base.useItems();
const selectedIdx = items.findIndex((item) => item.active);
@@ -222,7 +222,7 @@ function PageTOCPopoverTrigger({ className, ...props }: ComponentProps<'button'>
showItem && 'opacity-0 -translate-y-full pointer-events-none',
)}
>
{path?.name ?? text.toc}
{path?.name ?? t.toc}
</span>
<span
className={cn(
@@ -9,7 +9,7 @@ import {
useState,
} from 'react';
import { cn } from '@/utils/cn';
import { I18nLabel, useI18n } from '@/contexts/i18n';
import { I18nLabel, useTranslations } from '@/contexts/i18n';
import { TOC, TOCProvider, type TOCProviderProps, type TOCProps } from './slots/toc';
import { Footer, type FooterProps } from './slots/footer';
import { Breadcrumb, type BreadcrumbProps } from './slots/breadcrumb';
@@ -185,7 +185,7 @@ export function PageLastUpdate({
date: value,
...props
}: Omit<ComponentProps<'p'>, 'children'> & { date: Date }) {
const { text } = useI18n();
const t = useTranslations();
const [date, setDate] = useState('');
useEffect(() => {
@@ -195,7 +195,7 @@ export function PageLastUpdate({
return (
<p {...props} className={cn('text-sm text-fd-muted-foreground', props.className)}>
{text.lastUpdate} {date}
{t.lastUpdate} {date}
</p>
);
}
@@ -1,7 +1,7 @@
'use client';
import * as Base from '@/components/toc';
import { useI18n } from '@/contexts/i18n';
import { useTranslations } from '@/contexts/i18n';
import { useTreePath } from '@/contexts/tree';
import { cn } from '@/utils/cn';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
@@ -150,7 +150,7 @@ function PageTOCPopoverPhysical({ className, children, ...rest }: ComponentProps
}
function PageTOCPopoverTrigger({ className, ...props }: ComponentProps<'button'>) {
const { text } = useI18n();
const t = useTranslations();
const { open } = use(TocPopoverContext)!;
const items = Base.useItems();
const selectedIdx = items.findIndex((item) => item.active);
@@ -199,7 +199,7 @@ function PageTOCPopoverTrigger({ className, ...props }: ComponentProps<'button'>
</motion.span>
) : (
<motion.span key=":toc" {...spanProps}>
{text.toc}
{t.toc}
</motion.span>
)}
</AnimatePresence>
@@ -9,7 +9,7 @@ import {
useState,
} from 'react';
import { cn } from '@/utils/cn';
import { I18nLabel, useI18n } from '@/contexts/i18n';
import { I18nLabel, useTranslations } from '@/contexts/i18n';
import {
TOC,
TOCPopover,
@@ -206,7 +206,7 @@ export function PageLastUpdate({
date: value,
...props
}: Omit<ComponentProps<'p'>, 'children'> & { date: Date }) {
const { text } = useI18n();
const t = useTranslations();
const [date, setDate] = useState('');
useEffect(() => {
@@ -216,7 +216,7 @@ export function PageLastUpdate({
return (
<p {...props} className={cn('text-sm text-fd-muted-foreground', props.className)}>
{text.lastUpdate} {date}
{t.lastUpdate} {date}
</p>
);
}
@@ -1,6 +1,5 @@
'use client';
import { useI18n } from '@/contexts/i18n';
import { cn } from '@/utils/cn';
import { isActive } from '@/utils/urls';
import { useFooterItems } from '@/utils/use-footer-items';
@@ -9,6 +8,7 @@ import type * as PageTree from 'fumadocs-core/page-tree';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import Link from 'fumadocs-core/link';
import { type ComponentProps, useMemo } from 'react';
import { useTranslations } from '@/contexts/i18n';
type Item = Pick<PageTree.Item, 'name' | 'description' | 'url'>;
@@ -56,7 +56,7 @@ export function Footer({ items, children, className, ...props }: FooterProps) {
}
function FooterItem({ item, index }: { item: Item; index: 0 | 1 }) {
const { text } = useI18n();
const t = useTranslations();
const Icon = index === 0 ? ChevronLeft : ChevronRight;
return (
@@ -77,7 +77,7 @@ function FooterItem({ item, index }: { item: Item; index: 0 | 1 }) {
<p>{item.name}</p>
</div>
<p className="text-fd-muted-foreground truncate">
{item.description ?? (index === 0 ? text.previousPage : text.nextPage)}
{item.description ?? (index === 0 ? t.previousPage : t.nextPage)}
</p>
</Link>
);
@@ -2,7 +2,7 @@
import * as TocDefault from '@/components/toc/default';
import * as TocClerk from '@/components/toc/clerk';
import * as Base from '@/components/toc';
import { I18nLabel, useI18n } from '@/contexts/i18n';
import { I18nLabel, useTranslations } from '@/contexts/i18n';
import { cn } from '@/utils/cn';
import { ChevronDown, Text } from 'lucide-react';
import {
@@ -193,7 +193,7 @@ export function TOCPopover({
}
function PageTOCPopoverTrigger({ className, ...props }: ComponentProps<'button'>) {
const { text } = useI18n();
const t = useTranslations();
const { open } = use(TocPopoverContext)!;
const items = Base.useItems();
const selectedIdx = items.findIndex((item) => item.active);
@@ -222,7 +222,7 @@ function PageTOCPopoverTrigger({ className, ...props }: ComponentProps<'button'>
showItem && 'opacity-0 -translate-y-full pointer-events-none',
)}
>
{path?.name ?? text.toc}
{path?.name ?? t.toc}
</span>
<span
className={cn(
@@ -2,7 +2,7 @@
import type { ComponentProps } from 'react';
import { Search } from 'lucide-react';
import { useSearchContext } from '@/contexts/search';
import { useI18n } from '@/contexts/i18n';
import { useTranslations } from '@/contexts/i18n';
import { cn } from '@/utils/cn';
import { type ButtonProps, buttonVariants } from '@/components/ui/button';
@@ -46,7 +46,7 @@ export interface FullSearchTriggerProps extends ComponentProps<'button'> {
export function FullSearchTrigger({ hideIfDisabled, ...props }: FullSearchTriggerProps) {
const { enabled, hotKey, setOpenSearch } = useSearchContext();
const { text } = useI18n();
const t = useTranslations();
if (hideIfDisabled && !enabled) return null;
return (
@@ -63,7 +63,7 @@ export function FullSearchTrigger({ hideIfDisabled, ...props }: FullSearchTrigge
}}
>
<Search className="size-4" />
{text.search}
{t.search}
<div className="ms-auto inline-flex gap-0.5">
{hotKey.map((k, i) => (
<kbd key={i} className="rounded-md border bg-fd-background px-1.5">
+87 -3
View File
@@ -9,7 +9,7 @@ export interface I18nConfig<Languages extends string = string> {
/**
* Default locale if not specified
*/
defaultLanguage: Languages;
defaultLanguage: NoInfer<Languages>;
/**
* Don't show the locale prefix on URL.
@@ -36,11 +36,95 @@ export interface I18nConfig<Languages extends string = string> {
*
* Default to `defaultLanguage`, no fallback when set to `null`.
*/
fallbackLanguage?: Languages | null;
fallbackLanguage?: NoInfer<Languages> | null;
}
export function defineI18n<Languages extends string>(
export function defineI18n<const Languages extends string>(
config: I18nConfig<Languages>,
): I18nConfig<Languages> {
return config;
}
export type TranslationObject = Record<string, TranslationValue>;
export type TranslationValue<Params extends string = string> = string & { _params?: Params };
export interface TranslationsAPI<
Languages extends string = string,
Namespaces = Record<string, TranslationObject>,
> {
/** for type inference only, always `undefined` in runtime */
$inferLanguages: Languages;
/** for type inference only, always `undefined` in runtime */
$inferNamespaces: Namespaces;
config: I18nConfig<Languages>;
get: {
(lang: Languages): Namespaces;
(lang: string): Namespaces | undefined;
};
extend: <N extends string, Obj extends TranslationObject>(input: {
namespace: N;
defaultValue: Obj;
}) => TranslationsAPI<Languages, Namespaces & { [K in N]: Obj }>;
add: <N extends keyof Namespaces>(
namespace: N,
overrides: {
[Lang in Languages]?: Partial<Namespaces[N]>;
},
) => TranslationsAPI<Languages, Namespaces>;
}
export function defineTranslations<Languages extends string>(
config: I18nConfig<Languages>,
): TranslationsAPI<Languages, {}> {
const translations: Record<string, Record<string, TranslationObject>> = {};
for (const lang of config.languages) {
translations[lang] = {};
}
const api: TranslationsAPI = {
config,
$inferLanguages: undefined as never,
$inferNamespaces: undefined as never,
get(lang) {
return translations[lang];
},
add(namespace, overrides) {
for (const [lang, values] of Object.entries(overrides)) {
Object.assign(translations[lang][namespace], values);
}
return this as never;
},
extend({ namespace, defaultValue }: { namespace: string; defaultValue: TranslationObject }) {
for (const lang of config.languages) {
translations[lang][namespace] = { ...defaultValue };
}
return this as never;
},
};
return api as never;
}
export function renderTranslation(v: TranslationValue<never>): string;
export function renderTranslation<Params extends string>(
v: TranslationValue<Params>,
params: Record<Params, string>,
): string;
export function renderTranslation(
label: TranslationValue,
params?: Record<string, string>,
): string {
if (params) {
for (const k in params) {
label = label.replaceAll(`$${k}`, params[k]);
}
}
return label;
}
@@ -14,7 +14,7 @@ import {
useRef,
useState,
} from 'react';
import { I18nLabel, useI18n } from '@/contexts/i18n';
import { I18nLabel, useTranslations } from '@/contexts/i18n';
import { cn } from '@/utils/cn';
import { Dialog, DialogContent, DialogOverlay, DialogTitle } from '@radix-ui/react-dialog';
import type { HighlightedText, ReactSortedResult as BaseResultType } from 'fumadocs-core/search';
@@ -219,7 +219,7 @@ export function SearchDialogHeader(props: ComponentProps<'div'>) {
}
export function SearchDialogInput(props: ComponentProps<'input'>) {
const { text } = useI18n();
const t = useTranslations();
const { search, onSearchChange } = useSearch();
return (
@@ -227,7 +227,7 @@ export function SearchDialogInput(props: ComponentProps<'input'>) {
{...props}
value={search}
onChange={(e) => onSearchChange(e.target.value)}
placeholder={text.search}
placeholder={t.search}
className="w-0 flex-1 bg-transparent text-lg placeholder:text-fd-muted-foreground focus-visible:outline-none"
/>
);
@@ -276,7 +276,7 @@ export function SearchDialogOverlay(props: ComponentProps<typeof DialogOverlay>)
}
export function SearchDialogContent({ children, ...props }: ComponentProps<typeof DialogContent>) {
const { text } = useI18n();
const t = useTranslations();
return (
<DialogContent
@@ -288,7 +288,7 @@ export function SearchDialogContent({ children, ...props }: ComponentProps<typeo
props.className,
)}
>
<DialogTitle className="hidden">{text.search}</DialogTitle>
<DialogTitle className="hidden">{t.search}</DialogTitle>
{children}
</DialogContent>
);
@@ -12,7 +12,7 @@ import {
import { cn } from '@/utils/cn';
import { useTOCItems } from '.';
import { mergeRefs } from '@/utils/merge-refs';
import { useI18n } from '@/contexts/i18n';
import { useTranslations } from '@/contexts/i18n';
interface ComputedSVG {
width: number;
@@ -143,11 +143,11 @@ export function TOCItems({ ref, className, thumbBox = true, children, ...props }
}
export function TOCEmpty() {
const { text } = useI18n();
const t = useTranslations();
return (
<div className="rounded-lg border bg-fd-card p-3 text-xs text-fd-muted-foreground">
{text.tocNoHeadings}
{t.tocNoHeadings}
</div>
);
}
@@ -1,5 +1,5 @@
'use client';
import { useI18n } from '@/contexts/i18n';
import { useTranslations } from '@/contexts/i18n';
import { cn } from '@/utils/cn';
import { type ComponentProps, useCallback, useEffect, useRef, useState } from 'react';
import { mergeRefs } from '@/utils/merge-refs';
@@ -102,11 +102,11 @@ function TocThumb({ computed }: { computed: ComputedData }) {
}
export function TOCEmpty() {
const { text } = useI18n();
const t = useTranslations();
return (
<div className="rounded-lg border bg-fd-card p-3 text-xs text-fd-muted-foreground">
{text.tocNoHeadings}
{t.tocNoHeadings}
</div>
);
}
+27 -13
View File
@@ -1,8 +1,10 @@
'use client';
import { createContext, type ReactNode, useContext, useMemo, useRef } from 'react';
import { createContext, type ReactNode, use, useMemo, useRef } from 'react';
import { usePathname, useRouter } from 'fumadocs-core/framework';
import { renderTranslation, TranslationObject } from 'fumadocs-core/i18n';
export interface Translations {
export type Translations = {
displayName: string;
search: string;
searchNoResult: string;
@@ -15,10 +17,6 @@ export interface Translations {
previousPage: string;
chooseTheme: string;
editOnGithub: string;
}
export type TranslationsOption = {
[key: string]: string | TranslationsOption;
};
interface LocaleItem {
@@ -27,13 +25,14 @@ interface LocaleItem {
}
interface I18nContextType {
text: Translations & TranslationsOption;
text: Translations & Record<string, string | Record<string, string>>;
locale?: string;
onChange?: (v: string) => void;
locales?: LocaleItem[];
}
export const defaultTranslations: Translations = {
displayName: 'English',
search: 'Search',
searchNoResult: 'No results found',
toc: 'On this page',
@@ -47,16 +46,31 @@ export const defaultTranslations: Translations = {
};
const I18nContext = createContext<I18nContextType>({
text: { ...defaultTranslations },
text: defaultTranslations,
});
export function I18nLabel(props: { label: keyof Translations }): string {
const text = useI18n().text;
return text[props.label];
export function I18nLabel<
Obj extends TranslationObject = Translations,
K extends keyof Obj = keyof Obj,
>({
label,
namespace,
params,
}: {
label: K;
namespace?: string;
params?: Obj[K]['_params'] extends string ? Record<Obj[K]['_params'], string> : never;
}): string {
const t = useTranslations<Obj>(namespace);
return renderTranslation(t[label], params!);
}
export function useI18n(): I18nContextType {
return useContext(I18nContext);
return use(I18nContext);
}
export function useTranslations<Obj extends TranslationObject>(namespace?: string): Obj {
return (namespace ? use(I18nContext).text[namespace] : use(I18nContext).text) as Obj;
}
export interface I18nProviderProps {
@@ -73,7 +87,7 @@ export interface I18nProviderProps {
/**
* Translations of current locale
*/
translations?: TranslationsOption;
translations?: Partial<I18nContextType['text']>;
/**
* Available languages
+39 -6
View File
@@ -1,8 +1,44 @@
import type { I18nProviderProps, Translations } from '@/contexts/i18n';
import type { I18nConfig } from 'fumadocs-core/i18n';
import { defaultTranslations, type I18nProviderProps, type Translations } from '@/contexts/i18n';
import type { I18nConfig, TranslationsAPI } from 'fumadocs-core/i18n';
export { defaultTranslations, type I18nProviderProps, type Translations } from '@/contexts/i18n';
type TranslationsConfig<Languages extends string> = {
[K in Languages]?: Partial<Translations>;
};
export function uiTranslations(): {
namespace: 'ui';
defaultValues: Translations;
} {
return {
namespace: 'ui',
defaultValues: defaultTranslations,
};
}
export function i18nProvider<
Languages extends string,
P extends {
ui: Translations;
},
>(
translations: TranslationsAPI<Languages, P>,
lang: Languages | (string & {}) = translations.config.defaultLanguage,
): I18nProviderProps {
const { ui, ...rest } =
translations.get(lang) ?? translations.get(translations.config.defaultLanguage);
return {
locale: lang,
translations: { ...ui, ...rest },
locales: translations.config.languages.map((locale) => ({
locale,
name: translations.get(locale).ui.displayName ?? locale,
})),
};
}
export interface I18nUIConfig<Languages extends string> extends I18nConfig<Languages> {
/**
* get i18n config for Fumadocs UI `<RootProvider i18n={config} />`.
@@ -10,10 +46,7 @@ export interface I18nUIConfig<Languages extends string> extends I18nConfig<Langu
provider: (locale?: Languages | (string & {})) => I18nProviderProps;
}
type TranslationsConfig<Languages extends string> = {
[K in Languages]?: Partial<Translations> & { displayName?: string };
};
/** @deprecated use the `defineTranslations()` & `uiTranslations()` APIs instead */
export function defineI18nUI<Languages extends string>(
config: I18nConfig<Languages>,
options:
@@ -9,7 +9,7 @@ import {
useState,
} from 'react';
import { cn } from '@/utils/cn';
import { I18nLabel, useI18n } from '@/contexts/i18n';
import { I18nLabel, useTranslations } from '@/contexts/i18n';
import {
TOC,
TOCPopover,
@@ -207,7 +207,7 @@ export function PageLastUpdate({
date: value,
...props
}: Omit<ComponentProps<'p'>, 'children'> & { date: Date }) {
const { text } = useI18n();
const t = useTranslations();
const [date, setDate] = useState('');
useEffect(() => {
@@ -217,7 +217,7 @@ export function PageLastUpdate({
return (
<p {...props} className={cn('text-sm text-fd-muted-foreground', props.className)}>
{text.lastUpdate} {date}
{t.lastUpdate} {date}
</p>
);
}
@@ -1,6 +1,6 @@
'use client';
import { useI18n } from '@/contexts/i18n';
import { useI18n, useTranslations } from '@/contexts/i18n';
import { cn } from '@/utils/cn';
import { isActive } from '@/utils/urls';
import { useFooterItems } from '@/utils/use-footer-items';
@@ -56,7 +56,7 @@ export function Footer({ items, children, className, ...props }: FooterProps) {
}
function FooterItem({ item, index }: { item: Item; index: 0 | 1 }) {
const { text } = useI18n();
const t = useTranslations();
const Icon = index === 0 ? ChevronLeft : ChevronRight;
return (
@@ -77,7 +77,7 @@ function FooterItem({ item, index }: { item: Item; index: 0 | 1 }) {
<p>{item.name}</p>
</div>
<p className="text-fd-muted-foreground truncate">
{item.description ?? (index === 0 ? text.previousPage : text.nextPage)}
{item.description ?? (index === 0 ? t.previousPage : t.nextPage)}
</p>
</Link>
);
@@ -2,7 +2,7 @@
import * as TocDefault from '@/components/toc/default';
import * as TocClerk from '@/components/toc/clerk';
import * as Base from '@/components/toc';
import { I18nLabel, useI18n } from '@/contexts/i18n';
import { I18nLabel, useTranslations } from '@/contexts/i18n';
import { cn } from '@/utils/cn';
import { ChevronDown, Text } from 'lucide-react';
import {
@@ -193,7 +193,7 @@ export function TOCPopover({
}
function PageTOCPopoverTrigger({ className, ...props }: ComponentProps<'button'>) {
const { text } = useI18n();
const t = useTranslations();
const { open } = use(TocPopoverContext)!;
const items = Base.useItems();
const selectedIdx = items.findIndex((item) => item.active);
@@ -222,7 +222,7 @@ function PageTOCPopoverTrigger({ className, ...props }: ComponentProps<'button'>
showItem && 'opacity-0 -translate-y-full pointer-events-none',
)}
>
{path?.name ?? text.toc}
{path?.name ?? t.toc}
</span>
<span
className={cn(
@@ -9,7 +9,7 @@ import {
useState,
} from 'react';
import { cn } from '@/utils/cn';
import { I18nLabel, useI18n } from '@/contexts/i18n';
import { I18nLabel, useI18n, useTranslations } from '@/contexts/i18n';
import { TOC, TOCProvider, type TOCProviderProps, type TOCProps } from './slots/toc';
import { Footer, type FooterProps } from './slots/footer';
import { Breadcrumb, type BreadcrumbProps } from './slots/breadcrumb';
@@ -185,7 +185,7 @@ export function PageLastUpdate({
date: value,
...props
}: Omit<ComponentProps<'p'>, 'children'> & { date: Date }) {
const { text } = useI18n();
const t = useTranslations();
const [date, setDate] = useState('');
useEffect(() => {
@@ -195,7 +195,7 @@ export function PageLastUpdate({
return (
<p {...props} className={cn('text-sm text-fd-muted-foreground', props.className)}>
{text.lastUpdate} {date}
{t.lastUpdate} {date}
</p>
);
}
@@ -1,7 +1,7 @@
'use client';
import * as Base from '@/components/toc';
import { useI18n } from '@/contexts/i18n';
import { useI18n, useTranslations } from '@/contexts/i18n';
import { useTreePath } from '@/contexts/tree';
import { cn } from '@/utils/cn';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
@@ -150,7 +150,7 @@ function PageTOCPopoverPhysical({ className, children, ...rest }: ComponentProps
}
function PageTOCPopoverTrigger({ className, ...props }: ComponentProps<'button'>) {
const { text } = useI18n();
const t = useTranslations();
const { open } = use(TocPopoverContext)!;
const items = Base.useItems();
const selectedIdx = items.findIndex((item) => item.active);
@@ -199,7 +199,7 @@ function PageTOCPopoverTrigger({ className, ...props }: ComponentProps<'button'>
</motion.span>
) : (
<motion.span key=":toc" {...spanProps}>
{text.toc}
{t.toc}
</motion.span>
)}
</AnimatePresence>
@@ -9,7 +9,7 @@ import {
useState,
} from 'react';
import { cn } from '@/utils/cn';
import { I18nLabel, useI18n } from '@/contexts/i18n';
import { I18nLabel, useI18n, useTranslations } from '@/contexts/i18n';
import {
TOC,
TOCPopover,
@@ -206,7 +206,7 @@ export function PageLastUpdate({
date: value,
...props
}: Omit<ComponentProps<'p'>, 'children'> & { date: Date }) {
const { text } = useI18n();
const t = useTranslations();
const [date, setDate] = useState('');
useEffect(() => {
@@ -216,7 +216,7 @@ export function PageLastUpdate({
return (
<p {...props} className={cn('text-sm text-fd-muted-foreground', props.className)}>
{text.lastUpdate} {date}
{t.lastUpdate} {date}
</p>
);
}
@@ -1,6 +1,5 @@
'use client';
import { useI18n } from '@/contexts/i18n';
import { cn } from '@/utils/cn';
import { isActive } from '@/utils/urls';
import { useFooterItems } from '@/utils/use-footer-items';
@@ -9,6 +8,7 @@ import type * as PageTree from 'fumadocs-core/page-tree';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import Link from 'fumadocs-core/link';
import { type ComponentProps, useMemo } from 'react';
import { useTranslations } from '@/contexts/i18n';
type Item = Pick<PageTree.Item, 'name' | 'description' | 'url'>;
@@ -56,7 +56,7 @@ export function Footer({ items, children, className, ...props }: FooterProps) {
}
function FooterItem({ item, index }: { item: Item; index: 0 | 1 }) {
const { text } = useI18n();
const t = useTranslations();
const Icon = index === 0 ? ChevronLeft : ChevronRight;
return (
@@ -77,7 +77,7 @@ function FooterItem({ item, index }: { item: Item; index: 0 | 1 }) {
<p>{item.name}</p>
</div>
<p className="text-fd-muted-foreground truncate">
{item.description ?? (index === 0 ? text.previousPage : text.nextPage)}
{item.description ?? (index === 0 ? t.previousPage : t.nextPage)}
</p>
</Link>
);
@@ -2,7 +2,7 @@
import * as TocDefault from '@/components/toc/default';
import * as TocClerk from '@/components/toc/clerk';
import * as Base from '@/components/toc';
import { I18nLabel, useI18n } from '@/contexts/i18n';
import { I18nLabel, useTranslations } from '@/contexts/i18n';
import { cn } from '@/utils/cn';
import { ChevronDown, Text } from 'lucide-react';
import {
@@ -193,7 +193,7 @@ export function TOCPopover({
}
function PageTOCPopoverTrigger({ className, ...props }: ComponentProps<'button'>) {
const { text } = useI18n();
const t = useTranslations();
const { open } = use(TocPopoverContext)!;
const items = Base.useItems();
const selectedIdx = items.findIndex((item) => item.active);
@@ -222,7 +222,7 @@ function PageTOCPopoverTrigger({ className, ...props }: ComponentProps<'button'>
showItem && 'opacity-0 -translate-y-full pointer-events-none',
)}
>
{path?.name ?? text.toc}
{path?.name ?? t.toc}
</span>
<span
className={cn(
@@ -2,7 +2,7 @@
import type { ComponentProps } from 'react';
import { Search } from 'lucide-react';
import { useSearchContext } from '@/contexts/search';
import { useI18n } from '@/contexts/i18n';
import { useTranslations } from '@/contexts/i18n';
import { cn } from '@/utils/cn';
import { type ButtonProps, buttonVariants } from '@/components/ui/button';
@@ -46,7 +46,7 @@ export interface FullSearchTriggerProps extends ComponentProps<'button'> {
export function FullSearchTrigger({ hideIfDisabled, ...props }: FullSearchTriggerProps) {
const { enabled, hotKey, setOpenSearch } = useSearchContext();
const { text } = useI18n();
const t = useTranslations();
if (hideIfDisabled && !enabled) return null;
return (
@@ -63,7 +63,7 @@ export function FullSearchTrigger({ hideIfDisabled, ...props }: FullSearchTrigge
}}
>
<Search className="size-4" />
{text.search}
{t.search}
<div className="ms-auto inline-flex gap-0.5">
{hotKey.map((k, i) => (
<kbd key={i} className="rounded-md border bg-fd-background px-1.5">