mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Core: Support markdown files with default local code in file name
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-core': patch
|
||||
---
|
||||
|
||||
Support markdown files with default local code in file name
|
||||
@@ -1,16 +1,12 @@
|
||||
You can create a Markdown or meta file for specific language by adding `.{locale}` to your file name, like `page.fr.md` and `meta.fr.json`.
|
||||
You can add Markdown/meta files for different languages by attending `.{locale}` to your file name, like `page.fr.md` and `meta.fr.json`.
|
||||
|
||||
Be aware that you must create a file for the default locale first.
|
||||
Make sure to create a file for the default locale first, the locale code is optional (e.g. both `get-started.mdx` and `get-started.en.mdx` are accepted).
|
||||
|
||||
> For the default language, don't add the locale code to file name, just leave it like `get-started.mdx`.
|
||||
|
||||
### Example
|
||||
|
||||
Assume your default language is `en`.
|
||||
|
||||
| Name | |
|
||||
| ----------------- | --------------------------------------------- |
|
||||
| file.mdx | Correct |
|
||||
| file.cn.mdx | Correct |
|
||||
| file.en.mdx | Default locale doesn't need a locale code |
|
||||
| components.cn.mdx | Missing `components.mdx` for default language |
|
||||
<Files>
|
||||
<Folder name="content/docs" defaultOpen>
|
||||
<File name="meta.json" />
|
||||
<File name="meta.cn.json" />
|
||||
<File name="get-started.mdx" />
|
||||
<File name="get-started.cn.mdx" />
|
||||
</Folder>
|
||||
</Files>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import type { MetaData, PageData, UrlFn } from './types';
|
||||
import type { BuildPageTreeOptions } from './page-tree-builder';
|
||||
import { createPageTreeBuilder } from './page-tree-builder';
|
||||
import { type FileInfo } from './path';
|
||||
import { type FileInfo, getLocale } from './path';
|
||||
import type { MetaFile, PageFile, Storage } from './file-system';
|
||||
import * as path from 'node:path';
|
||||
|
||||
@@ -156,21 +156,25 @@ function indexPages(
|
||||
if (item.format === 'meta')
|
||||
pathToFile.set(item.file.path, fileToMeta(item));
|
||||
|
||||
if (item.format === 'page') {
|
||||
const page = fileToPage(item, getUrl, item.file.locale);
|
||||
if (
|
||||
item.format === 'page' &&
|
||||
(item.file.locale ?? defaultLanguage) === defaultLanguage
|
||||
) {
|
||||
const page = fileToPage(item, getUrl, defaultLanguage);
|
||||
pathToFile.set(item.file.path, page);
|
||||
|
||||
if (item.file.locale) continue;
|
||||
map.set(`${defaultLanguage}.${page.slugs.join('/')}`, page);
|
||||
if (!i18n) continue;
|
||||
const basePath = getLocale(item.file.flattenedPath)[0];
|
||||
|
||||
for (const lang of i18n.languages) {
|
||||
const localized = storage.read(
|
||||
`${item.file.flattenedPath}.${lang}`,
|
||||
'page',
|
||||
);
|
||||
if (lang === defaultLanguage) continue;
|
||||
|
||||
const localizedPage = fileToPage(localized ?? item, getUrl, lang);
|
||||
const localizedPage = fileToPage(
|
||||
storage.read(`${basePath}.${lang}`, 'page') ?? item,
|
||||
getUrl,
|
||||
lang,
|
||||
);
|
||||
map.set(`${lang}.${localizedPage.slugs.join('/')}`, localizedPage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,15 @@ import type * as PageTree from '../server/page-tree';
|
||||
import type { File, Folder, MetaFile, PageFile, Storage } from './file-system';
|
||||
import { resolvePath } from '@/utils/path';
|
||||
import { type UrlFn } from './types';
|
||||
import { getLocale } from '@/source/path';
|
||||
|
||||
interface PageTreeBuilderContext {
|
||||
lang?: string;
|
||||
defaultLanguage?: string;
|
||||
|
||||
storage: Storage;
|
||||
builder: PageTreeBuilder;
|
||||
options: BuildPageTreeOptions;
|
||||
|
||||
i18n?: I18nConfig;
|
||||
}
|
||||
|
||||
export interface BuildPageTreeOptions {
|
||||
@@ -77,11 +77,20 @@ function buildAll(
|
||||
const output: PageTree.Node[] = [];
|
||||
const folders: PageTree.Folder[] = [];
|
||||
|
||||
for (const node of [...nodes].sort((a, b) =>
|
||||
for (const node of nodes.toSorted((a, b) =>
|
||||
a.file.name.localeCompare(b.file.name),
|
||||
)) {
|
||||
if (isPageFile(node) && !node.file.locale) {
|
||||
const treeNode = buildFileNode(node, ctx);
|
||||
const locale = node.file.locale ?? ctx.defaultLanguage;
|
||||
|
||||
if (isPageFile(node) && locale === ctx.defaultLanguage) {
|
||||
const localized = findFileOfLocale(
|
||||
node.file.flattenedPath,
|
||||
ctx.lang,
|
||||
'page',
|
||||
ctx,
|
||||
);
|
||||
|
||||
const treeNode = buildFileNode(localized ?? node, ctx);
|
||||
|
||||
if (node.file.name === 'index') {
|
||||
if (!skipIndex) output.unshift(treeNode);
|
||||
@@ -92,14 +101,12 @@ function buildAll(
|
||||
}
|
||||
|
||||
if ('children' in node) {
|
||||
if (
|
||||
node.children.length === 1 &&
|
||||
node.children[0].file.name === 'index' &&
|
||||
isPageFile(node.children[0])
|
||||
) {
|
||||
output.push(buildFileNode(node.children[0], ctx));
|
||||
const folder = buildFolderNode(node, false, ctx);
|
||||
|
||||
if (folder.children.length === 0 && folder.index) {
|
||||
output.push(folder.index);
|
||||
} else {
|
||||
folders.push(buildFolderNode(node, false, ctx));
|
||||
folders.push(folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,7 +165,21 @@ function resolveFolderItem(
|
||||
|
||||
const path = resolvePath(folder.file.path, filename);
|
||||
|
||||
const itemNode = ctx.storage.readDir(path) ?? ctx.storage.read(path, 'page');
|
||||
let itemNode: Folder | PageFile | undefined = ctx.storage.readDir(path);
|
||||
|
||||
if (!itemNode) {
|
||||
itemNode = ctx.storage.read(path, 'page');
|
||||
|
||||
// find localized page
|
||||
// use target locale if no forced locale, or specified file doesn't exist
|
||||
if (ctx.lang && !itemNode?.file.locale) {
|
||||
itemNode =
|
||||
findFileOfLocale(path, ctx.lang, 'page', ctx) ??
|
||||
// fallback to default
|
||||
findFileOfLocale(path, ctx.defaultLanguage, 'page', ctx);
|
||||
}
|
||||
}
|
||||
|
||||
if (!itemNode) return [];
|
||||
|
||||
addedNodePaths.add(itemNode.file.path);
|
||||
@@ -180,8 +201,9 @@ function buildFolderNode(
|
||||
): PageTree.Folder {
|
||||
const metaPath = resolvePath(folder.file.path, 'meta');
|
||||
const meta =
|
||||
findLocalizedFile(metaPath, 'meta', ctx) ??
|
||||
ctx.storage.read(metaPath, 'meta');
|
||||
findFileOfLocale(metaPath, ctx.lang, 'meta', ctx) ??
|
||||
findFileOfLocale(metaPath, ctx.defaultLanguage, 'meta', ctx);
|
||||
|
||||
const indexFile = ctx.storage.read(
|
||||
resolvePath(folder.file.flattenedPath, 'index'),
|
||||
'page',
|
||||
@@ -251,19 +273,16 @@ function buildFileNode(
|
||||
file: PageFile,
|
||||
ctx: PageTreeBuilderContext,
|
||||
): PageTree.Item {
|
||||
const localized =
|
||||
findLocalizedFile(file.file.flattenedPath, 'page', ctx) ?? file;
|
||||
|
||||
const item: PageTree.Item = {
|
||||
$id: localized.file.path,
|
||||
$id: file.file.path,
|
||||
type: 'page',
|
||||
name: localized.data.data.title ?? pathToName(localized.file.name),
|
||||
description: localized.data.data.description,
|
||||
icon: ctx.options.resolveIcon?.(localized.data.data.icon),
|
||||
url: ctx.options.getUrl(localized.data.slugs, ctx.lang),
|
||||
name: file.data.data.title ?? pathToName(file.file.name),
|
||||
description: file.data.data.description,
|
||||
icon: ctx.options.resolveIcon?.(file.data.data.icon),
|
||||
url: ctx.options.getUrl(file.data.slugs, ctx.lang),
|
||||
$ref: !ctx.options.noRef
|
||||
? {
|
||||
file: localized.file.path,
|
||||
file: file.file.path,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
@@ -297,7 +316,7 @@ export function createPageTreeBuilder(): PageTreeBuilder {
|
||||
options,
|
||||
builder: this,
|
||||
storage: options.storage,
|
||||
i18n,
|
||||
defaultLanguage: i18n.defaultLanguage,
|
||||
});
|
||||
|
||||
return [lang, tree];
|
||||
@@ -308,14 +327,22 @@ export function createPageTreeBuilder(): PageTreeBuilder {
|
||||
};
|
||||
}
|
||||
|
||||
function findLocalizedFile<F extends File['format']>(
|
||||
function findFileOfLocale<F extends File['format']>(
|
||||
path: string,
|
||||
locale: string | undefined | null,
|
||||
format: F,
|
||||
ctx: PageTreeBuilderContext,
|
||||
): Extract<File, { format: F }> | undefined {
|
||||
if (!ctx.lang) return;
|
||||
const pathWithoutLocale = getLocale(path)[0];
|
||||
|
||||
return ctx.storage.read(`${path}.${ctx.lang}`, format);
|
||||
if (locale) {
|
||||
const result = ctx.storage.read(`${pathWithoutLocale}.${locale}`, format);
|
||||
if (result) return result;
|
||||
}
|
||||
|
||||
if ((locale ?? ctx.defaultLanguage) === ctx.defaultLanguage) {
|
||||
return ctx.storage.read(pathWithoutLocale, format);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -62,7 +62,7 @@ export function parseFolderPath(path: string): FileInfo {
|
||||
};
|
||||
}
|
||||
|
||||
function getLocale(name: string): [string, string?] {
|
||||
export function getLocale(name: string): [string, string?] {
|
||||
const sep = name.lastIndexOf('.');
|
||||
if (sep === -1) return [name];
|
||||
const locale = name.slice(sep + 1);
|
||||
|
||||
@@ -226,7 +226,7 @@ const i18nSource: Source<SourceConfig> = {
|
||||
},
|
||||
{
|
||||
type: 'meta',
|
||||
path: 'meta.json',
|
||||
path: 'meta.en.json',
|
||||
data: {
|
||||
title: 'Docs English',
|
||||
pages: ['test', 'nested'],
|
||||
@@ -242,7 +242,7 @@ const i18nSource: Source<SourceConfig> = {
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
path: '/nested/test.mdx',
|
||||
path: '/nested/test.en.mdx',
|
||||
data: {
|
||||
title: 'Nested Page',
|
||||
},
|
||||
@@ -302,9 +302,9 @@ test('Internationalized Routing', () => {
|
||||
"$ref": {},
|
||||
"children": [
|
||||
{
|
||||
"$id": "nested/test.mdx",
|
||||
"$id": "nested/test.en.mdx",
|
||||
"$ref": {
|
||||
"file": "nested/test.mdx",
|
||||
"file": "nested/test.en.mdx",
|
||||
},
|
||||
"name": "Nested Page",
|
||||
"type": "page",
|
||||
@@ -356,45 +356,6 @@ test('Internationalized Routing', () => {
|
||||
}
|
||||
`);
|
||||
|
||||
expect(result.getPages()).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"title": "Hello",
|
||||
},
|
||||
"file": {
|
||||
"dirname": "",
|
||||
"flattenedPath": "test",
|
||||
"locale": undefined,
|
||||
"name": "test",
|
||||
"path": "test.mdx",
|
||||
},
|
||||
"locale": "en",
|
||||
"slugs": [
|
||||
"test",
|
||||
],
|
||||
"url": "/en/test",
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"title": "Nested Page",
|
||||
},
|
||||
"file": {
|
||||
"dirname": "nested",
|
||||
"flattenedPath": "nested/test",
|
||||
"locale": undefined,
|
||||
"name": "test",
|
||||
"path": "nested/test.mdx",
|
||||
},
|
||||
"locale": "en",
|
||||
"slugs": [
|
||||
"nested",
|
||||
"test",
|
||||
],
|
||||
"url": "/en/nested/test",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(result.getLanguages()).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
@@ -463,10 +424,10 @@ test('Internationalized Routing', () => {
|
||||
},
|
||||
"file": {
|
||||
"dirname": "nested",
|
||||
"flattenedPath": "nested/test",
|
||||
"locale": undefined,
|
||||
"flattenedPath": "nested/test.en",
|
||||
"locale": "en",
|
||||
"name": "test",
|
||||
"path": "nested/test.mdx",
|
||||
"path": "nested/test.en.mdx",
|
||||
},
|
||||
"locale": "en",
|
||||
"slugs": [
|
||||
@@ -511,7 +472,7 @@ test('Internationalized Routing: Hide Prefix', () => {
|
||||
"$id": "nested",
|
||||
"children": [
|
||||
{
|
||||
"$id": "nested/test.mdx",
|
||||
"$id": "nested/test.en.mdx",
|
||||
"name": "Nested Page",
|
||||
"type": "page",
|
||||
"url": "/nested/test",
|
||||
|
||||
Reference in New Issue
Block a user