mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Core: Introduce new translations API
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user