diff --git a/.changeset/rare-garlics-hammer.md b/.changeset/rare-garlics-hammer.md
new file mode 100644
index 000000000..c61705ae7
--- /dev/null
+++ b/.changeset/rare-garlics-hammer.md
@@ -0,0 +1,7 @@
+---
+'fumadocs-core': patch
+'@fumadocs/cli': patch
+'fumadocs-ui': patch
+---
+
+Improve i18n api
diff --git a/apps/docs/content/docs/ui/internationalization.mdx b/apps/docs/content/docs/ui/internationalization.mdx
index 195e1e328..891c76ffb 100644
--- a/apps/docs/content/docs/ui/internationalization.mdx
+++ b/apps/docs/content/docs/ui/internationalization.mdx
@@ -49,24 +49,6 @@ export const source = loader({
});
```
-Update the usages to your source to include a locale code:
-
-```ts
-import { source } from '@/lib/source';
-
-// get page tree
-source.pageTree[params.lang];
-
-// get page
-source.getPage(params.slug, params.lang);
-
-// get pages
-source.getPages(params.lang);
-```
-
-Note that without providing a locale code, it uses your default locale instead.
-You can see [Source API](/docs/headless/source-api) for other usages.
-
### Middleware
Create a middleware that redirects users to appropriate locale.
@@ -83,28 +65,57 @@ Create a middleware that redirects users to appropriate locale.
See [Middleware](/docs/headless/internationalization#middleware) for customisable options.
-### Root Layout
+### Routing
Create a dynamic route `/app/[lang]`, and move all special files from `/app` to
the folder.
-A `I18nProvider` is needed for localization. Wrap the root provider inside your I18n provider.
+A `I18nProvider` is needed for localization.
+Wrap the root provider inside your I18n provider, and provide available languages & translations to it.
+
+Note that only English translations are provided by default.
```tsx
import { RootProvider } from 'fumadocs-ui/provider';
-import { I18nProvider } from 'fumadocs-ui/i18n';
+import { I18nProvider, type Translations } from 'fumadocs-ui/i18n';
+
+const cn: Translations = {
+ search: 'Translated Content',
+ // other props
+};
-export default function RootLayout({
+// available languages that will be displayed on UI
+// make sure `locale` is consistent with your i18n config
+const locales = [
+ {
+ name: 'English',
+ locale: 'en',
+ },
+ {
+ name: 'Chinese',
+ locale: 'cn',
+ },
+];
+
+export default async function RootLayout({
params,
children,
}: {
- params: { lang: string };
+ params: Promise<{ lang: string }>;
children: React.ReactNode;
}) {
return (
-
+
-
+ {children}
@@ -113,47 +124,61 @@ export default function RootLayout({
}
```
-### Writing Documents
+### Source
-see [Page Conventions](/docs/ui/page-conventions#internationalization) to learn how to organize your documents.
+Update the usages to your source to include a locale code:
-### Search
+```ts
+import { source } from '@/lib/source';
-Configure i18n on your search solution.
+// get page tree
+source.pageTree[params.lang];
-You don't need further changes if you're using the `createFromSource` shortcut.
+// get page
+source.getPage(params.slug, params.lang);
-For the built-in Orama search, see [Search I18n](/docs/headless/search/orama#internationalization).
+// get pages
+source.getPages(params.lang);
+```
-### Adding Translations
+like:
-We only provide English translation by default, you have to pass your translations to the provider.
+```tsx title="app/[lang]/layout.tsx"
+import { source } from '@/lib/source';
+import { DocsLayout } from 'fumadocs-ui/docs';
+import type { ReactNode } from 'react';
-```tsx
-import { I18nProvider } from 'fumadocs-ui/i18n';
-
-;
+export default async function Layout({
+ params,
+ children,
+}: {
+ params: Promise<{ lang: string }>;
+ children: ReactNode;
+}) {
+ const pageTree = source.pageTree[(await params).lang];
+
+ return {children};
+}
```
+Note that without providing a locale code, it uses your default locale instead.
+You can see [Source API](/docs/headless/source-api) for other usages.
+
+### Writing Documents
+
+see [Page Conventions](/docs/ui/page-conventions#internationalization) to learn how to organize your documents.
+
+### Search
+
+Configure i18n on your search solution.
+
+- Built-in Search (Orama):
+ - For `createFromSource` and most languages, no further changes are needed.
+ - For special languages like Chinese & Japanese, they require additional config.
+ See [Orama I18n](/docs/headless/search/orama#internationalization) guide.
+- Cloud Solutions (e.g. Algolia):
+ - They usually have official support for multilingual.
+
### Add Language Switch
To allow users changing their language, enable `i18n` on your layouts.
diff --git a/examples/i18n/app/api/search/route.ts b/examples/i18n/app/api/search/route.ts
index e38b20464..a15d0f483 100644
--- a/examples/i18n/app/api/search/route.ts
+++ b/examples/i18n/app/api/search/route.ts
@@ -4,7 +4,7 @@ import { createFromSource } from 'fumadocs-core/search/server';
import { createTokenizer } from '@orama/tokenizers/mandarin';
import { stopwords } from '@orama/stopwords/mandarin';
-export const { GET, search } = createFromSource(source, undefined, {
+export const { GET } = createFromSource(source, undefined, {
localeMap: {
// the prop name should be its locale code in your i18n config, (e.g. `cn`)
cn: {
diff --git a/packages/cli/src/plugins/i18n.ts b/packages/cli/src/plugins/i18n.ts
index 19a859e36..3b37e5307 100644
--- a/packages/cli/src/plugins/i18n.ts
+++ b/packages/cli/src/plugins/i18n.ts
@@ -27,10 +27,10 @@ export const i18nPlugin: Plugin = {
type: 'code',
title: 'page.tsx',
code: `
-export default function Page({
+export default async function Page({
params,
}: {
- ${picocolors.underline(picocolors.bold('params: { lang: string; slug?: string[] };'))}
+ ${picocolors.underline(picocolors.bold('params: Promise<{ lang: string; slug?: string[] }>'))}
})
`.trim(),
},
diff --git a/packages/core/src/search/client/static.ts b/packages/core/src/search/client/static.ts
index 3addb9017..0c2b09988 100644
--- a/packages/core/src/search/client/static.ts
+++ b/packages/core/src/search/client/static.ts
@@ -6,10 +6,12 @@ import {
type RawData,
} from '@orama/orama';
import { type SortedResult } from '@/server';
-import { searchSimple } from '@/search/search/simple';
-import { searchAdvanced } from '@/search/search/advanced';
-import { type advancedSchema } from '@/search/create-db';
-import { type schema } from '@/search/create-db-simple';
+import { searchSimple } from '@/search/orama/search/simple';
+import { searchAdvanced } from '@/search/orama/search/advanced';
+import {
+ type advancedSchema,
+ type simpleSchema,
+} from '@/search/orama/create-db';
export interface StaticOptions {
/**
@@ -86,7 +88,10 @@ export function createStaticClient({
if (!cached) return [];
if (cached.type === 'simple')
- return searchSimple(cached as unknown as Orama, query);
+ return searchSimple(
+ cached as unknown as Orama,
+ query,
+ );
return searchAdvanced(
cached.db as Orama,
diff --git a/packages/core/src/search/create-db-simple.ts b/packages/core/src/search/create-db-simple.ts
deleted file mode 100644
index ef8e484a1..000000000
--- a/packages/core/src/search/create-db-simple.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import {
- create,
- insertMultiple,
- type Orama,
- type TypedDocument,
-} from '@orama/orama';
-import { type SimpleOptions } from '@/search/server';
-
-export type SimpleDocument = TypedDocument>;
-export const schema = {
- url: 'string',
- title: 'string',
- description: 'string',
- content: 'string',
- keywords: 'string',
-} as const;
-
-export async function createDBSimple({
- indexes,
- tokenizer,
- ...rest
-}: SimpleOptions): Promise> {
- const items = typeof indexes === 'function' ? await indexes() : indexes;
- const db = (await create({
- schema,
- components: {
- tokenizer,
- },
- ...rest,
- })) as unknown as Orama;
-
- await insertMultiple(
- db,
- items.map((page) => ({
- title: page.title,
- description: page.description,
- url: page.url,
- content: page.content,
- keywords: page.keywords,
- })),
- );
-
- return db;
-}
diff --git a/packages/core/src/search/_stemmers.ts b/packages/core/src/search/orama/_stemmers.ts
similarity index 100%
rename from packages/core/src/search/_stemmers.ts
rename to packages/core/src/search/orama/_stemmers.ts
diff --git a/packages/core/src/search/create-db.ts b/packages/core/src/search/orama/create-db.ts
similarity index 68%
rename from packages/core/src/search/create-db.ts
rename to packages/core/src/search/orama/create-db.ts
index a7a598b3f..bc78097b0 100644
--- a/packages/core/src/search/create-db.ts
+++ b/packages/core/src/search/orama/create-db.ts
@@ -5,7 +5,7 @@ import {
type PartialSchemaDeep,
type TypedDocument,
} from '@orama/orama';
-import { type AdvancedOptions } from '@/search/server';
+import { type AdvancedOptions, type SimpleOptions } from '@/search/server';
export type AdvancedDocument = TypedDocument>;
export const advancedSchema = {
@@ -85,3 +85,40 @@ export async function createDB({
await insertMultiple(db, mapTo);
return db;
}
+
+export type SimpleDocument = TypedDocument>;
+export const simpleSchema = {
+ url: 'string',
+ title: 'string',
+ description: 'string',
+ content: 'string',
+ keywords: 'string',
+} as const;
+
+export async function createDBSimple({
+ indexes,
+ tokenizer,
+ ...rest
+}: SimpleOptions): Promise> {
+ const items = typeof indexes === 'function' ? await indexes() : indexes;
+ const db = (await create({
+ schema: simpleSchema,
+ components: {
+ tokenizer,
+ },
+ ...rest,
+ })) as unknown as Orama;
+
+ await insertMultiple(
+ db,
+ items.map((page) => ({
+ title: page.title,
+ description: page.description,
+ url: page.url,
+ content: page.content,
+ keywords: page.keywords,
+ })),
+ );
+
+ return db;
+}
diff --git a/packages/core/src/search/create-endpoint.ts b/packages/core/src/search/orama/create-endpoint.ts
similarity index 100%
rename from packages/core/src/search/create-endpoint.ts
rename to packages/core/src/search/orama/create-endpoint.ts
diff --git a/packages/core/src/search/create-from-source.ts b/packages/core/src/search/orama/create-from-source.ts
similarity index 96%
rename from packages/core/src/search/create-from-source.ts
rename to packages/core/src/search/orama/create-from-source.ts
index 3d59edde3..85773b446 100644
--- a/packages/core/src/search/create-from-source.ts
+++ b/packages/core/src/search/orama/create-from-source.ts
@@ -12,7 +12,7 @@ import {
type Page,
} from '@/source';
import { type StructuredData } from '@/mdx-plugins';
-import { type LocaleMap } from '@/search/i18n-api';
+import { type LocaleMap } from '@/search/orama/create-i18n';
function pageToIndex(page: Page): AdvancedIndex {
if (!('structuredData' in page.data)) {
diff --git a/packages/core/src/search/i18n-api.ts b/packages/core/src/search/orama/create-i18n.ts
similarity index 96%
rename from packages/core/src/search/i18n-api.ts
rename to packages/core/src/search/orama/create-i18n.ts
index 734daa52d..896a33358 100644
--- a/packages/core/src/search/i18n-api.ts
+++ b/packages/core/src/search/orama/create-i18n.ts
@@ -10,9 +10,9 @@ import {
type SearchServer,
type SimpleOptions,
} from '@/search/server';
-import { createEndpoint } from '@/search/create-endpoint';
+import { createEndpoint } from '@/search/orama/create-endpoint';
import { type I18nConfig } from '@/i18n';
-import { STEMMERS } from '@/search/_stemmers';
+import { STEMMERS } from '@/search/orama/_stemmers';
export type LocaleMap = Record;
diff --git a/packages/core/src/search/search/advanced.ts b/packages/core/src/search/orama/search/advanced.ts
similarity index 94%
rename from packages/core/src/search/search/advanced.ts
rename to packages/core/src/search/orama/search/advanced.ts
index 6bfe7326d..2b8622264 100644
--- a/packages/core/src/search/search/advanced.ts
+++ b/packages/core/src/search/orama/search/advanced.ts
@@ -1,5 +1,8 @@
import { getByID, type Orama, search, type SearchParams } from '@orama/orama';
-import { type AdvancedDocument, type advancedSchema } from '@/search/create-db';
+import {
+ type AdvancedDocument,
+ type advancedSchema,
+} from '@/search/orama/create-db';
import { removeUndefined } from '@/utils/remove-undefined';
import type { SortedResult } from '@/server';
diff --git a/packages/core/src/search/search/simple.ts b/packages/core/src/search/orama/search/simple.ts
similarity index 72%
rename from packages/core/src/search/search/simple.ts
rename to packages/core/src/search/orama/search/simple.ts
index 7e97be0d9..a9c565057 100644
--- a/packages/core/src/search/search/simple.ts
+++ b/packages/core/src/search/orama/search/simple.ts
@@ -1,11 +1,16 @@
import { type Orama, search, type SearchParams } from '@orama/orama';
import type { SortedResult } from '@/server';
-import { type schema, type SimpleDocument } from '@/search/create-db-simple';
+import {
+ type simpleSchema,
+ type SimpleDocument,
+} from '@/search/orama/create-db';
export async function searchSimple(
- db: Orama,
+ db: Orama,
query: string,
- params: Partial, SimpleDocument>> = {},
+ params: Partial<
+ SearchParams, SimpleDocument>
+ > = {},
): Promise {
const result = await search(db, {
term: query,
diff --git a/packages/core/src/search/server.ts b/packages/core/src/search/server.ts
index ce5cddc28..ab9968ef4 100644
--- a/packages/core/src/search/server.ts
+++ b/packages/core/src/search/server.ts
@@ -2,19 +2,17 @@ import { type Orama, type SearchParams, save, create } from '@orama/orama';
import { type NextRequest } from 'next/server';
import type { StructuredData } from '@/mdx-plugins/remark-structure';
import type { SortedResult } from '@/server/types';
-import { createEndpoint } from '@/search/create-endpoint';
+import { createEndpoint } from '@/search/orama/create-endpoint';
import {
type AdvancedDocument,
type advancedSchema,
createDB,
-} from '@/search/create-db';
-import { searchSimple } from '@/search/search/simple';
-import { searchAdvanced } from '@/search/search/advanced';
-import {
createDBSimple,
- type schema,
type SimpleDocument,
-} from './create-db-simple';
+ simpleSchema,
+} from '@/search/orama/create-db';
+import { searchSimple } from '@/search/orama/search/simple';
+import { searchAdvanced } from '@/search/orama/search/advanced';
export interface SearchServer {
search: (
@@ -57,7 +55,7 @@ export interface SimpleOptions extends SharedOptions {
/**
* Customise search options on server
*/
- search?: Partial, SimpleDocument>>;
+ search?: Partial, SimpleDocument>>;
}
export interface AdvancedOptions extends SharedOptions {
@@ -133,7 +131,7 @@ export function initAdvancedSearch(options: AdvancedOptions): SearchServer {
async export() {
return {
type: 'advanced',
- ...save(await get),
+ ...(await save(await get)),
};
},
async search(query, searchOptions) {
@@ -144,5 +142,5 @@ export function initAdvancedSearch(options: AdvancedOptions): SearchServer {
};
}
-export { createFromSource } from './create-from-source';
-export { createI18nSearchAPI } from './i18n-api';
+export { createFromSource } from './orama/create-from-source';
+export { createI18nSearchAPI } from './orama/create-i18n';
diff --git a/packages/ui/src/components/dialog/search-algolia.tsx b/packages/ui/src/components/dialog/search-algolia.tsx
index 108532b8a..babe04808 100644
--- a/packages/ui/src/components/dialog/search-algolia.tsx
+++ b/packages/ui/src/components/dialog/search-algolia.tsx
@@ -6,8 +6,12 @@ import {
} from 'fumadocs-core/search/client';
import { type ReactNode, useState } from 'react';
import { useOnChange } from 'fumadocs-core/utils/use-on-change';
-import { SearchDialog, type SharedProps } from './search';
-import { type TagItem, TagsList } from './tag-list';
+import {
+ SearchDialog,
+ type SharedProps,
+ TagsList,
+ type TagItem,
+} from './search';
export interface AlgoliaSearchDialogProps extends SharedProps {
index: AlgoliaOptions['index'];
diff --git a/packages/ui/src/components/dialog/search-default.tsx b/packages/ui/src/components/dialog/search-default.tsx
index f6732e346..bc3d3e691 100644
--- a/packages/ui/src/components/dialog/search-default.tsx
+++ b/packages/ui/src/components/dialog/search-default.tsx
@@ -4,8 +4,12 @@ import { useDocsSearch } from 'fumadocs-core/search/client';
import { type ReactNode, useState } from 'react';
import { useOnChange } from 'fumadocs-core/utils/use-on-change';
import { useI18n } from '@/contexts/i18n';
-import { SearchDialog, type SharedProps } from './search';
-import { type TagItem, TagsList } from './tag-list';
+import {
+ SearchDialog,
+ type SharedProps,
+ type TagItem,
+ TagsList,
+} from './search';
export interface DefaultSearchDialogProps extends SharedProps {
/**
diff --git a/packages/ui/src/components/dialog/search-orama.tsx b/packages/ui/src/components/dialog/search-orama.tsx
index 12de7c689..7f17fcda5 100644
--- a/packages/ui/src/components/dialog/search-orama.tsx
+++ b/packages/ui/src/components/dialog/search-orama.tsx
@@ -6,8 +6,12 @@ import {
} from 'fumadocs-core/search/client';
import { type ReactNode, useState } from 'react';
import { useOnChange } from 'fumadocs-core/utils/use-on-change';
-import { SearchDialog, type SharedProps } from './search';
-import { type TagItem, TagsList } from './tag-list';
+import {
+ SearchDialog,
+ type SharedProps,
+ type TagItem,
+ TagsList,
+} from './search';
export interface OramaSearchDialogProps extends SharedProps {
client: OramaCloudOptions['client'];
diff --git a/packages/ui/src/components/dialog/search.tsx b/packages/ui/src/components/dialog/search.tsx
index 53252befa..e45e9ea2a 100644
--- a/packages/ui/src/components/dialog/search.tsx
+++ b/packages/ui/src/components/dialog/search.tsx
@@ -10,6 +10,7 @@ import {
useRef,
type ButtonHTMLAttributes,
useCallback,
+ type HTMLAttributes,
} from 'react';
import { useI18n } from '@/contexts/i18n';
import { cn } from '@/utils/cn';
@@ -23,6 +24,7 @@ import {
DialogTitle,
} from '@radix-ui/react-dialog';
import type { SortedResult } from 'fumadocs-core/server';
+import { cva } from 'class-variance-authority';
export type SearchLink = [name: string, href: string];
@@ -287,3 +289,68 @@ function CommandItem({
);
}
+
+export interface TagItem {
+ name: string;
+ value: string | undefined;
+
+ props?: HTMLAttributes;
+}
+
+export interface TagsListProps extends HTMLAttributes {
+ tag?: string;
+ onTagChange: (tag: string | undefined) => void;
+ allowClear?: boolean;
+
+ items: TagItem[];
+}
+
+const itemVariants = cva(
+ 'rounded-md border px-2 py-0.5 text-xs font-medium text-fd-muted-foreground transition-colors',
+ {
+ variants: {
+ active: {
+ true: 'bg-fd-accent text-fd-accent-foreground',
+ },
+ },
+ },
+);
+
+export function TagsList({
+ tag,
+ onTagChange,
+ items,
+ allowClear,
+ ...props
+}: TagsListProps) {
+ return (
+