mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
React Router: rewrite i18n example
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
---
|
||||
'fumadocs-core': minor
|
||||
---
|
||||
|
||||
**Include locale code into `page.path`**
|
||||
|
||||
Previously when i18n is enabled, `page.path` is not equal to the virtual file paths you passed into `loader()`:
|
||||
|
||||
```ts
|
||||
const source = loader({
|
||||
source: {
|
||||
files: [{
|
||||
path: 'folder/index.cn.mdx'
|
||||
// ...
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
console.log(source.getPages("cn"));
|
||||
// path: folder/index.mdx
|
||||
```
|
||||
|
||||
This can be confusing, the only solution to obtain the original path was `page.absolutePath`.
|
||||
|
||||
From now, the `page.path` will also include the locale code:
|
||||
|
||||
```ts
|
||||
const source = loader({
|
||||
source: {
|
||||
files: [{
|
||||
path: 'folder/index.cn.mdx'
|
||||
// ...
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
console.log(source.getPages('cn'));
|
||||
// path: folder/index.cn.mdx
|
||||
```
|
||||
|
||||
While this change doesn't affect intended API usages, it **may lead to minor bugs** when advanced usage/hacks involved around `page.path`.
|
||||
@@ -1,11 +1,12 @@
|
||||
import { type MetaData, type PageData } from '@/source/types';
|
||||
import { FileSystem } from './file-system';
|
||||
import { slash, splitPath } from '@/utils/path';
|
||||
import { VirtualFile } from '@/source/loader';
|
||||
import { joinPath, slash, splitPath } from '@/utils/path';
|
||||
import { type VirtualFile } from '@/source/loader';
|
||||
import { basename, dirname } from '@/source/path';
|
||||
|
||||
export interface LoadOptions {
|
||||
transformers?: Transformer[];
|
||||
buildFiles: (files: VirtualFile[]) => (MetaFile | PageFile)[];
|
||||
buildFile: (file: VirtualFile) => MetaFile | PageFile;
|
||||
}
|
||||
|
||||
export type ContentStorage = FileSystem<MetaFile | PageFile>;
|
||||
@@ -32,90 +33,83 @@ export type Transformer = (context: {
|
||||
options: LoadOptions;
|
||||
}) => void;
|
||||
|
||||
function isLocaleValid(locale: string) {
|
||||
return locale.length > 0 && !/\d+/.test(locale);
|
||||
}
|
||||
|
||||
const parsers = {
|
||||
dir(path: string): [string, string?] {
|
||||
const [locale, ...segs] = path.split('/');
|
||||
|
||||
if (locale && segs.length > 0 && isLocaleValid(locale))
|
||||
return [segs.join('/'), locale];
|
||||
|
||||
return [path];
|
||||
},
|
||||
dot(path: string): [string, string?] {
|
||||
const dir = dirname(path);
|
||||
const base = basename(path);
|
||||
const parts = base.split('.');
|
||||
if (parts.length < 3) return [path];
|
||||
|
||||
const [locale] = parts.splice(parts.length - 2, 1);
|
||||
if (!isLocaleValid(locale)) return [path];
|
||||
|
||||
return [joinPath(dir, parts.join('.')), locale];
|
||||
},
|
||||
none(path: string): [string, string?] {
|
||||
return [path];
|
||||
},
|
||||
};
|
||||
|
||||
// Virtual files -> Virtual Storage -> Transformers -> Result
|
||||
/**
|
||||
* @returns a map of locale and its content storage.
|
||||
*
|
||||
* in the storage, locale codes are removed from file paths, hence the same file will have same file paths in every storage.
|
||||
*/
|
||||
export function loadFiles(
|
||||
files: VirtualFile[],
|
||||
options: LoadOptions,
|
||||
): ContentStorage {
|
||||
const { transformers = [] } = options;
|
||||
const storage: ContentStorage = new FileSystem();
|
||||
const normalized: VirtualFile[] = files.map((file) => ({
|
||||
...file,
|
||||
path: normalizePath(file.path),
|
||||
}));
|
||||
|
||||
for (const item of options.buildFiles(normalized)) {
|
||||
storage.write(item.path, item);
|
||||
}
|
||||
|
||||
for (const transformer of transformers) {
|
||||
transformer({
|
||||
storage,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
export function loadFilesI18n(
|
||||
files: VirtualFile[],
|
||||
options: LoadOptions,
|
||||
i18n: {
|
||||
parser: 'dot' | 'dir';
|
||||
parser: keyof typeof parsers;
|
||||
languages: string[];
|
||||
defaultLanguage: string;
|
||||
},
|
||||
): Record<string, ContentStorage> {
|
||||
const parser = i18n.parser === 'dir' ? dirParser : dotParser;
|
||||
const { buildFile, transformers = [] } = options;
|
||||
const parser = parsers[i18n.parser];
|
||||
const storages: Record<string, ContentStorage> = {};
|
||||
const normalized = files.map((file) =>
|
||||
buildFile({
|
||||
...file,
|
||||
path: normalizePath(file.path),
|
||||
}),
|
||||
);
|
||||
|
||||
for (const lang of i18n.languages) {
|
||||
storages[lang] = loadFiles(
|
||||
files.flatMap((file) => {
|
||||
const [path, locale] = parser(normalizePath(file.path));
|
||||
const storage: ContentStorage = new FileSystem();
|
||||
|
||||
if ((locale ?? i18n.defaultLanguage) === lang) {
|
||||
return {
|
||||
...file,
|
||||
path,
|
||||
};
|
||||
}
|
||||
for (const item of normalized) {
|
||||
const [path, locale = i18n.defaultLanguage] = parser(item.path);
|
||||
|
||||
return [];
|
||||
}),
|
||||
options,
|
||||
);
|
||||
if (locale !== lang) continue;
|
||||
storage.write(path, item);
|
||||
}
|
||||
|
||||
for (const transformer of transformers) {
|
||||
transformer({
|
||||
storage,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
storages[lang] = storage;
|
||||
}
|
||||
|
||||
return storages;
|
||||
}
|
||||
|
||||
function dirParser(path: string): [string, string?] {
|
||||
const parsed = path.split('/');
|
||||
if (parsed.length >= 2) return [parsed.slice(1).join('/'), parsed[0]];
|
||||
|
||||
return [path];
|
||||
}
|
||||
|
||||
function dotParser(path: string): [string, string?] {
|
||||
const segs = path.split('/');
|
||||
if (segs.length === 0) return [path];
|
||||
|
||||
const name = segs[segs.length - 1].split('.');
|
||||
if (name.length >= 3) {
|
||||
const locale = name.splice(name.length - 2, 1)[0];
|
||||
|
||||
if (locale.length > 0 && !/\d+/.test(locale)) {
|
||||
segs[segs.length - 1] = name.join('.');
|
||||
|
||||
return [segs.join('/'), locale];
|
||||
}
|
||||
}
|
||||
|
||||
return [path];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path - Relative path
|
||||
* @returns Normalized path, with no trailing/leading slashes
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { I18nConfig } from '@/i18n';
|
||||
import {
|
||||
type ContentStorage,
|
||||
loadFiles,
|
||||
loadFilesI18n,
|
||||
type MetaFile,
|
||||
type PageFile,
|
||||
type Transformer,
|
||||
@@ -213,17 +212,15 @@ function indexPages(
|
||||
|
||||
for (const filePath of storages[defaultLanguage].getFiles()) {
|
||||
const item = storages[defaultLanguage].read(filePath)!;
|
||||
const path = `${defaultLanguage}.${filePath}`;
|
||||
|
||||
if (item.format === 'meta') {
|
||||
result.pathToMeta.set(
|
||||
`${defaultLanguage}.${item.path}`,
|
||||
fileToMeta(item),
|
||||
);
|
||||
result.pathToMeta.set(path, fileToMeta(item));
|
||||
}
|
||||
|
||||
if (item.format === 'page') {
|
||||
const page = fileToPage(item, getUrl, defaultLanguage);
|
||||
result.pathToPage.set(`${defaultLanguage}.${item.path}`, page);
|
||||
result.pathToPage.set(path, page);
|
||||
result.pages.set(`${defaultLanguage}.${page.slugs.join('/')}`, page);
|
||||
|
||||
if (!i18n) continue;
|
||||
@@ -238,7 +235,7 @@ function indexPages(
|
||||
);
|
||||
|
||||
if (localizedItem) {
|
||||
result.pathToPage.set(`${lang}.${item.path}`, localizedPage);
|
||||
result.pathToPage.set(`${lang}.${filePath}`, localizedPage);
|
||||
}
|
||||
|
||||
result.pages.set(
|
||||
@@ -299,78 +296,76 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
|
||||
i18n,
|
||||
slugs: slugsFn,
|
||||
url: getUrl = createGetUrl(baseUrl ?? '/', i18n),
|
||||
transformers,
|
||||
transformers = [],
|
||||
} = options;
|
||||
const defaultLanguage = i18n?.defaultLanguage ?? '';
|
||||
const files =
|
||||
typeof source.files === 'function' ? source.files() : source.files;
|
||||
|
||||
function buildFiles(files: VirtualFile[]) {
|
||||
const indexFiles: VirtualFile[] = [];
|
||||
const transformerSlugs: Transformer = ({ storage }) => {
|
||||
const indexFiles = new Set<string>();
|
||||
const taken = new Set<string>();
|
||||
|
||||
for (const file of files) {
|
||||
if (file.type !== 'page' || file.slugs) continue;
|
||||
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(file.path) && !slugsFn) {
|
||||
indexFiles.push(file);
|
||||
if (isIndex(path) && !slugsFn) {
|
||||
indexFiles.add(path);
|
||||
continue;
|
||||
}
|
||||
|
||||
file.slugs = slugsFn
|
||||
? slugsFn(parseFilePath(file.path))
|
||||
: getSlugs(file.path);
|
||||
file.slugs = slugsFn ? slugsFn(parseFilePath(path)) : getSlugs(path);
|
||||
|
||||
const key = file.slugs.join('/');
|
||||
if (taken.has(key)) throw new Error('Duplicated slugs');
|
||||
taken.add(key);
|
||||
}
|
||||
|
||||
for (const file of indexFiles) {
|
||||
file.slugs = getSlugs(file.path);
|
||||
for (const path of indexFiles) {
|
||||
const file = storage.read(path);
|
||||
if (file?.format !== 'page') continue;
|
||||
|
||||
file.slugs = getSlugs(path);
|
||||
if (taken.has(file.slugs.join('/'))) file.slugs.push('index');
|
||||
}
|
||||
};
|
||||
|
||||
const storages = loadFiles(
|
||||
files,
|
||||
{
|
||||
buildFile(file) {
|
||||
if (file.type === 'page') {
|
||||
return {
|
||||
format: 'page',
|
||||
path: file.path,
|
||||
slugs: file.slugs,
|
||||
data: file.data,
|
||||
absolutePath: file.absolutePath ?? '',
|
||||
} as PageFile;
|
||||
}
|
||||
|
||||
return files.map((file) => {
|
||||
if (file.type === 'page') {
|
||||
return {
|
||||
format: 'page',
|
||||
format: 'meta',
|
||||
path: file.path,
|
||||
slugs: file.slugs!,
|
||||
data: file.data,
|
||||
absolutePath: file.absolutePath ?? '',
|
||||
} as PageFile;
|
||||
}
|
||||
|
||||
return {
|
||||
format: 'meta',
|
||||
path: file.path,
|
||||
absolutePath: file.absolutePath ?? '',
|
||||
data: file.data,
|
||||
} as MetaFile;
|
||||
});
|
||||
}
|
||||
|
||||
const storages = i18n
|
||||
? loadFilesI18n(
|
||||
files,
|
||||
{
|
||||
buildFiles,
|
||||
transformers,
|
||||
},
|
||||
{
|
||||
data: file.data,
|
||||
} as MetaFile;
|
||||
},
|
||||
transformers: [transformerSlugs, ...transformers],
|
||||
},
|
||||
i18n
|
||||
? {
|
||||
...i18n,
|
||||
parser: i18n.parser ?? 'dot',
|
||||
}
|
||||
: {
|
||||
defaultLanguage,
|
||||
parser: 'none',
|
||||
languages: [defaultLanguage],
|
||||
},
|
||||
)
|
||||
: {
|
||||
'': loadFiles(files, {
|
||||
transformers,
|
||||
buildFiles,
|
||||
}),
|
||||
};
|
||||
);
|
||||
|
||||
const walker = indexPages(storages, getUrl, i18n);
|
||||
const builder = createPageTreeBuilder(getUrl);
|
||||
|
||||
+12
-12
@@ -10,12 +10,12 @@
|
||||
"file": {
|
||||
"dirname": "",
|
||||
"ext": ".mdx",
|
||||
"flattenedPath": "test",
|
||||
"name": "test",
|
||||
"path": "test.mdx",
|
||||
"flattenedPath": "test.cn",
|
||||
"name": "test.cn",
|
||||
"path": "test.cn.mdx",
|
||||
},
|
||||
"locale": "cn",
|
||||
"path": "test.mdx",
|
||||
"path": "test.cn.mdx",
|
||||
"slugs": [
|
||||
"test",
|
||||
],
|
||||
@@ -29,12 +29,12 @@
|
||||
"file": {
|
||||
"dirname": "nested",
|
||||
"ext": ".mdx",
|
||||
"flattenedPath": "nested/test",
|
||||
"name": "test",
|
||||
"path": "nested/test.mdx",
|
||||
"flattenedPath": "nested/test.cn",
|
||||
"name": "test.cn",
|
||||
"path": "nested/test.cn.mdx",
|
||||
},
|
||||
"locale": "cn",
|
||||
"path": "nested/test.mdx",
|
||||
"path": "nested/test.cn.mdx",
|
||||
"slugs": [
|
||||
"nested",
|
||||
"test",
|
||||
@@ -73,12 +73,12 @@
|
||||
"file": {
|
||||
"dirname": "nested",
|
||||
"ext": ".mdx",
|
||||
"flattenedPath": "nested/test",
|
||||
"name": "test",
|
||||
"path": "nested/test.mdx",
|
||||
"flattenedPath": "nested/test.en",
|
||||
"name": "test.en",
|
||||
"path": "nested/test.en.mdx",
|
||||
},
|
||||
"locale": "en",
|
||||
"path": "nested/test.mdx",
|
||||
"path": "nested/test.en.mdx",
|
||||
"slugs": [
|
||||
"nested",
|
||||
"test",
|
||||
|
||||
Reference in New Issue
Block a user