mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Core: Support fallbackLanguage for loader() API
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-core': patch
|
||||
---
|
||||
|
||||
Support `fallbackLanguage` for `loader()` i18n API
|
||||
@@ -3,7 +3,7 @@ title: Internationalization
|
||||
description: Support multiple languages in your documentation
|
||||
---
|
||||
|
||||
## Introduction
|
||||
## Define Config
|
||||
|
||||
Fumadocs core provides necessary middleware and options for i18n support.
|
||||
|
||||
@@ -18,13 +18,13 @@ You can define a config to share between utilities.
|
||||
To hide the locale prefix (e.g. `/en/page` -> `/page`), use the `hideLocale` option.
|
||||
|
||||
```ts
|
||||
import type { I18nConfig } from 'fumadocs-core/i18n';
|
||||
import { defineI18n } from 'fumadocs-core/i18n';
|
||||
|
||||
export const i18n: I18nConfig = {
|
||||
export const i18n = defineI18n({
|
||||
defaultLanguage: 'en',
|
||||
languages: ['en', 'cn'],
|
||||
hideLocale: 'default-locale',
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
| Mode | Description |
|
||||
@@ -41,7 +41,26 @@ This may cause undesired cache problems, and need to pay extra attention on SEO
|
||||
|
||||
</Callout>
|
||||
|
||||
### Middleware
|
||||
### Fallback Language
|
||||
|
||||
The fallback language to use when translations are missing for a page, default to your `defaultLanguage`.
|
||||
|
||||
Supported:
|
||||
|
||||
- A language in your `languages` array.
|
||||
- When set to `null`, not fallback will be used.
|
||||
|
||||
```ts
|
||||
import { defineI18n } from 'fumadocs-core/i18n';
|
||||
|
||||
export const i18n = defineI18n({
|
||||
languages: ['en', 'cn'],
|
||||
defaultLanguage: 'en',
|
||||
fallbackLanguage: 'cn',
|
||||
});
|
||||
```
|
||||
|
||||
## Middleware
|
||||
|
||||
Redirects users to appropriate locale, it can be customised from `i18n.ts`.
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ export const source = loader({
|
||||
|
||||
With i18n enabled, loader will generate a page tree for every locale.
|
||||
|
||||
When looking for a page, it fallbacks to default locale if the page doesn't exist for specified locale.
|
||||
If translations are missing for a page, it fallbacks to [`fallbackLanguage`](/docs/headless/internationalization#fallback-language).
|
||||
|
||||
## Output
|
||||
|
||||
|
||||
@@ -29,7 +29,14 @@ export interface I18nConfig<Languages extends string = string> {
|
||||
*
|
||||
* @defaultValue 'dot'
|
||||
*/
|
||||
parser?: 'dot' | 'dir';
|
||||
parser?: 'dot' | 'dir' | 'none';
|
||||
|
||||
/**
|
||||
* the fallback language when the page has no translations available for a given locale.
|
||||
*
|
||||
* Default to ``defaultLanguage`, no fallback when set to `null`.
|
||||
*/
|
||||
fallbackLanguage?: Languages | null;
|
||||
}
|
||||
|
||||
export function defineI18n<Languages extends string>(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FileSystem } from './file-system';
|
||||
import { joinPath, slash, splitPath } from '@/utils/path';
|
||||
import { type VirtualFile } from '@/source/loader';
|
||||
import { basename, dirname } from '@/source/path';
|
||||
import type { I18nConfig } from '@/i18n';
|
||||
|
||||
export interface LoadOptions {
|
||||
transformers?: Transformer[];
|
||||
@@ -74,14 +75,10 @@ const parsers = {
|
||||
export function loadFiles(
|
||||
files: VirtualFile[],
|
||||
options: LoadOptions,
|
||||
i18n: {
|
||||
parser: keyof typeof parsers;
|
||||
languages: string[];
|
||||
defaultLanguage: string;
|
||||
},
|
||||
i18n: I18nConfig,
|
||||
): Record<string, ContentStorage> {
|
||||
const { buildFile, transformers = [] } = options;
|
||||
const parser = parsers[i18n.parser];
|
||||
const parser = parsers[i18n.parser ?? 'dot'];
|
||||
const storages: Record<string, ContentStorage> = {};
|
||||
const normalized = files.map((file) =>
|
||||
buildFile({
|
||||
@@ -89,9 +86,21 @@ export function loadFiles(
|
||||
path: normalizePath(file.path),
|
||||
}),
|
||||
);
|
||||
const fallbackLang =
|
||||
i18n.fallbackLanguage !== null
|
||||
? (i18n.fallbackLanguage ?? i18n.defaultLanguage)
|
||||
: null;
|
||||
|
||||
function scan(lang: string, fallback?: ContentStorage): ContentStorage {
|
||||
const storage: ContentStorage = new FileSystem(fallback);
|
||||
function scan(lang: string) {
|
||||
if (storages[lang]) return;
|
||||
|
||||
let storage: ContentStorage;
|
||||
if (fallbackLang && fallbackLang !== lang) {
|
||||
scan(fallbackLang);
|
||||
storage = new FileSystem(storages[fallbackLang]);
|
||||
} else {
|
||||
storage = new FileSystem();
|
||||
}
|
||||
|
||||
for (const item of normalized) {
|
||||
const [path, locale = i18n.defaultLanguage] = parser(item.path);
|
||||
@@ -106,15 +115,10 @@ export function loadFiles(
|
||||
});
|
||||
}
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
storages[i18n.defaultLanguage] = scan(i18n.defaultLanguage);
|
||||
|
||||
for (const lang of i18n.languages) {
|
||||
storages[lang] ??= scan(lang, storages[i18n.defaultLanguage]);
|
||||
storages[lang] = storage;
|
||||
}
|
||||
|
||||
for (const lang of i18n.languages) scan(lang);
|
||||
return storages;
|
||||
}
|
||||
|
||||
|
||||
@@ -274,13 +274,14 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
|
||||
const transformerSlugs: Transformer = ({ storage }) => {
|
||||
const indexFiles = new Set<string>();
|
||||
const taken = new Set<string>();
|
||||
// for custom slugs function, don't handle conflicting cases like `dir/index.mdx` vs `dir.mdx`
|
||||
const autoIndex = slugsFn === undefined;
|
||||
|
||||
for (const path of storage.getFiles()) {
|
||||
const file = storage.read(path);
|
||||
if (!file || file.format !== 'page' || file.slugs) continue;
|
||||
|
||||
// for custom slugs function, don't handle conflicting cases like `dir/index.mdx` vs `dir.mdx`
|
||||
if (isIndex(path) && !slugsFn) {
|
||||
if (isIndex(path) && autoIndex) {
|
||||
indexFiles.add(path);
|
||||
continue;
|
||||
}
|
||||
@@ -324,16 +325,11 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
|
||||
},
|
||||
transformers: [transformerSlugs, ...transformers],
|
||||
},
|
||||
i18n
|
||||
? {
|
||||
...i18n,
|
||||
parser: i18n.parser ?? 'dot',
|
||||
}
|
||||
: {
|
||||
defaultLanguage,
|
||||
parser: 'none',
|
||||
languages: [defaultLanguage],
|
||||
},
|
||||
i18n ?? {
|
||||
defaultLanguage,
|
||||
parser: 'none',
|
||||
languages: [defaultLanguage],
|
||||
},
|
||||
);
|
||||
|
||||
const walker = indexPages(storages, getUrl);
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface PageTreeBuilderContext<
|
||||
Meta extends MetaData = MetaData,
|
||||
> {
|
||||
/**
|
||||
* @internal
|
||||
* @internal resolve paths without extensions
|
||||
*/
|
||||
resolveName: (name: string, format: 'meta' | 'page') => string;
|
||||
options: BaseOptions<Page, Meta>;
|
||||
@@ -70,6 +70,12 @@ export interface BaseOptions<
|
||||
noRef?: boolean;
|
||||
transformers?: PageTreeTransformer<Page, Meta>[];
|
||||
resolveIcon?: (icon: string | undefined) => ReactNode | undefined;
|
||||
/**
|
||||
* generate fallback page tree
|
||||
*
|
||||
* @defaultValue true
|
||||
*/
|
||||
generateFallback?: boolean;
|
||||
}
|
||||
|
||||
export interface PageTreeBuilder<
|
||||
@@ -79,15 +85,7 @@ export interface PageTreeBuilder<
|
||||
build: (
|
||||
options: BaseOptions<Page, Meta> & {
|
||||
id?: string;
|
||||
|
||||
storage: ContentStorage<Page, Meta>;
|
||||
|
||||
/**
|
||||
* generate fallback page tree
|
||||
*
|
||||
* @defaultValue true
|
||||
*/
|
||||
generateFallback?: boolean;
|
||||
},
|
||||
) => PageTree.Root;
|
||||
|
||||
@@ -347,7 +345,7 @@ function build(id: string, ctx: PageTreeBuilderContext): PageTree.Root {
|
||||
}
|
||||
|
||||
export function createPageTreeBuilder(getUrl: UrlFn): PageTreeBuilder {
|
||||
function getTransformers(options: BaseOptions, generateFallback = true) {
|
||||
function getTransformers(options: BaseOptions, generateFallback: boolean) {
|
||||
const transformers: PageTreeTransformer[] = [legacyTransformer(options)];
|
||||
|
||||
if (options.transformers) {
|
||||
@@ -377,28 +375,24 @@ export function createPageTreeBuilder(getUrl: UrlFn): PageTreeBuilder {
|
||||
}
|
||||
|
||||
return {
|
||||
build({ storage, id, generateFallback, ...options }) {
|
||||
const resolve = createFlattenPathResolver(storage);
|
||||
build({ storage, id, ...options }) {
|
||||
const key = '';
|
||||
|
||||
return build(id ?? 'root', {
|
||||
transformers: getTransformers(options, generateFallback),
|
||||
options,
|
||||
builder: this,
|
||||
storage,
|
||||
getUrl,
|
||||
resolveName(name, format) {
|
||||
return resolve(name, format) ?? name;
|
||||
},
|
||||
});
|
||||
return this.buildI18n({
|
||||
id,
|
||||
storages: { [key]: storage },
|
||||
...options,
|
||||
})[key];
|
||||
},
|
||||
buildI18n({ id, storages, ...options }) {
|
||||
const transformers = getTransformers(options);
|
||||
buildI18n({ id, storages, generateFallback = true, ...options }) {
|
||||
const transformers = getTransformers(options, generateFallback);
|
||||
const out: Record<string, PageTree.Root> = {};
|
||||
|
||||
for (const [locale, storage] of Object.entries(storages)) {
|
||||
const resolve = createFlattenPathResolver(storage);
|
||||
const branch = locale.length === 0 ? 'root' : locale;
|
||||
|
||||
out[locale] = build(id ?? (locale.length === 0 ? 'root' : locale), {
|
||||
out[locale] = build(id ? `${id}-${branch}` : branch, {
|
||||
transformers,
|
||||
builder: this,
|
||||
options,
|
||||
|
||||
Reference in New Issue
Block a user