Core & UI: Support type-safe i18n config

This commit is contained in:
Fuma Nama
2025-08-19 16:58:45 +08:00
parent 78aa97bc14
commit 50eb07f51d
8 changed files with 116 additions and 24 deletions
+51
View File
@@ -0,0 +1,51 @@
---
'fumadocs-core': minor
'fumadocs-ui': minor
---
**Support type-safe i18n config**
```ts
// lib/source.ts
import { defineI18n } from 'fumadocs-core/i18n';
export const i18n = defineI18n({
defaultLanguage: 'en',
languages: ['en', 'cn'],
});
```
```tsx
// root layout
import { defineI18nUI } from 'fumadocs-ui/i18n';
import { i18n } from '@/lib/i18n';
const { provider } = defineI18nUI(i18n, {
translations: {
cn: {
displayName: 'Chinese',
search: 'Translated Content'
},
en: {
displayName: 'English'
}
}
});
function RootLayout({ children }: { children: React.ReactNode }) {
return (
<RootProvider i18n={provider(lang)}>{children}</RootProvider>
);
}
```
Although optional, we highly recommend you to refactor the import to i18n middleware:
```ts
// here!
import { createI18nMiddleware } from 'fumadocs-core/i18n/middleware';
import { i18n } from '@/lib/i18n';
export default createI18nMiddleware(i18n);
};
```
+2
View File
@@ -0,0 +1,2 @@
export * from './index';
export * from './middleware';
+9 -5
View File
@@ -1,17 +1,15 @@
export * from './middleware';
export interface I18nConfig {
export interface I18nConfig<Languages extends string = string> {
/**
* Supported locale codes.
*
* A page tree will be built for each language.
*/
languages: string[];
languages: Languages[];
/**
* Default locale if not specified
*/
defaultLanguage: string;
defaultLanguage: Languages;
/**
* Don't show the locale prefix on URL.
@@ -33,3 +31,9 @@ export interface I18nConfig {
*/
parser?: 'dot' | 'dir';
}
export function defineI18n<Languages extends string>(
config: I18nConfig<Languages>,
): I18nConfig<Languages> {
return config;
}
@@ -12,8 +12,9 @@ import {
type Page,
} from '@/source';
import { type StructuredData } from '@/mdx-plugins';
import { type LocaleMap } from '@/search/orama/create-i18n';
import { basename, extname } from '@/source/path';
import type { I18nConfig } from '@/i18n';
import type { Language } from '@orama/orama';
function pageToIndex(page: Page): AdvancedIndex {
if (!('structuredData' in page.data)) {
@@ -36,14 +37,21 @@ function pageToIndex(page: Page): AdvancedIndex {
};
}
interface Options<Page = unknown> extends Omit<AdvancedOptions, 'indexes'> {
localeMap?: LocaleMap<Partial<AdvancedOptions>>;
buildIndex?: (page: Page) => AdvancedIndex;
interface Options<S extends LoaderOutput<LoaderConfig>>
extends Omit<AdvancedOptions, 'indexes'> {
localeMap?: {
[K in S extends LoaderOutput<infer C>
? C['i18n'] extends I18nConfig<infer Languages>
? Languages
: string
: string]?: Partial<AdvancedOptions> | Language;
};
buildIndex?: (page: InferPageType<S>) => AdvancedIndex;
}
export function createFromSource<S extends LoaderOutput<LoaderConfig>>(
source: S,
options?: Options<InferPageType<S>>,
options?: Options<S>,
): SearchAPI;
/**
@@ -52,17 +60,17 @@ export function createFromSource<S extends LoaderOutput<LoaderConfig>>(
export function createFromSource<S extends LoaderOutput<LoaderConfig>>(
source: S,
pageToIndexFn?: (page: InferPageType<S>) => AdvancedIndex,
options?: Omit<Options, 'buildIndex'>,
options?: Omit<Options<S>, 'buildIndex'>,
): SearchAPI;
export function createFromSource<S extends LoaderOutput<LoaderConfig>>(
source: S,
_buildIndexOrOptions:
| ((page: InferPageType<S>) => AdvancedIndex)
| Options<InferPageType<S>> = pageToIndex,
_options?: Omit<Options, 'buildIndex'>,
| Options<S> = pageToIndex,
_options?: Omit<Options<S>, 'buildIndex'>,
): SearchAPI {
const options: Options<InferPageType<S>> = {
const options: Options<S> = {
...(typeof _buildIndexOrOptions === 'function'
? {
buildIndex: _buildIndexOrOptions,
@@ -14,8 +14,6 @@ import { createEndpoint } from '@/search/orama/create-endpoint';
import { type I18nConfig } from '@/i18n';
import { STEMMERS } from '@/search/orama/_stemmers';
export type LocaleMap<O> = Record<string, Language | O>;
type I18nOptions<O extends SimpleOptions | AdvancedOptions, Idx> = Omit<
O,
'language' | 'indexes'
@@ -25,7 +23,7 @@ type I18nOptions<O extends SimpleOptions | AdvancedOptions, Idx> = Omit<
/**
* Map locale name from i18n config to Orama compatible `language` or options
*/
localeMap?: LocaleMap<Partial<O>>;
localeMap?: Record<string, Language | Partial<O> | undefined>;
indexes: WithLocale<Idx>[] | Dynamic<WithLocale<Idx>>;
};
+3 -3
View File
@@ -23,7 +23,7 @@ import { joinPath } from '@/utils/path';
export interface LoaderConfig {
source: SourceConfig;
i18n: boolean;
i18n: I18nConfig | undefined;
}
export interface SourceConfig {
@@ -126,7 +126,7 @@ export interface LanguageEntry<Data = PageData> {
}
export interface LoaderOutput<Config extends LoaderConfig> {
pageTree: Config['i18n'] extends true
pageTree: Config['i18n'] extends I18nConfig
? Record<string, PageTree.Root>
: PageTree.Root;
@@ -279,7 +279,7 @@ export function loader<
options: LoaderOptions<Config, I18n>,
): LoaderOutput<{
source: Config;
i18n: I18n extends I18nConfig ? true : false;
i18n: I18n;
}> {
// @ts-expect-error -- forced type cast
return createOutput(options);
+1 -1
View File
@@ -19,7 +19,7 @@ export default defineConfig({
'src/utils/use-on-change.ts',
'src/utils/use-effect-event.ts',
'src/utils/use-media-query.ts',
'src/i18n/index.ts',
'src/i18n/*.ts',
'src/highlight/index.ts',
'src/highlight/client.tsx',
'src/framework/*',
+32 -3
View File
@@ -2,11 +2,16 @@
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 {
defaultTranslations,
I18nContext,
type Translations,
} from '@/contexts/i18n';
import type { I18nProviderProps } from './provider/base';
import type { I18nConfig } from 'fumadocs-core/i18n';
export type { I18nProviderProps };
export { defaultTranslations, type Translations } from './contexts/i18n';
export type { I18nProviderProps, Translations };
export { defaultTranslations } from './contexts/i18n';
// TODO: remove next major
/**
@@ -62,3 +67,27 @@ export function I18nProvider({
</I18nContext.Provider>
);
}
export function defineI18nUI<Languages extends string>(
config: I18nConfig<Languages>,
options: {
translations: {
[K in Languages]?: Partial<Translations> & { displayName?: string };
};
},
) {
const { translations } = options;
return {
provider(locale: string = config.defaultLanguage): I18nProviderProps {
return {
locale,
translations: translations[locale as Languages],
locales: config.languages.map((locale) => ({
locale,
name: translations[locale]?.displayName ?? locale,
})),
};
},
};
}