Compare commits

...

8 Commits

Author SHA1 Message Date
sebastienlorber e9b20ca990 refactor, create getLocalizedSource() 2024-01-04 20:08:49 +01:00
sebastienlorber ecf6888840 dogfooding editUrl typo 2024-01-04 19:47:10 +01:00
sebastienlorber f27bedb76f dogfooding editUrl typo 2024-01-04 19:30:18 +01:00
sebastienlorber b49b462389 extract code to docusaurus utils 2024-01-04 18:59:50 +01:00
sebastienlorber 90b1895156 refactor PagesContentPaths type 2024-01-04 18:35:33 +01:00
sebastienlorber 87b9df54c8 add explicit blog post date 2024-01-04 18:33:35 +01:00
sebastienlorber b4f95929cf add docs and blog dogfood files 2024-01-04 18:30:16 +01:00
sebastienlorber fc72669528 Add support for locale extensions in pages plugin 2024-01-04 18:19:54 +01:00
17 changed files with 239 additions and 42 deletions

View File

@ -26,6 +26,8 @@ import {
getContentPathList,
isUnlisted,
isDraft,
filterFilesWithLocaleExtension,
getLocalizedSource,
} from '@docusaurus/utils';
import {validateBlogPostFrontMatter} from './frontMatter';
import {type AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors';
@ -208,13 +210,19 @@ async function parseBlogPostMarkdownFile({
const defaultReadingTime: ReadingTimeFunction = ({content, options}) =>
readingTime(content, options).minutes;
async function processBlogSourceFile(
blogSourceRelative: string,
contentPaths: BlogContentPaths,
context: LoadContext,
options: PluginOptions,
authorsMap?: AuthorsMap,
): Promise<BlogPost | undefined> {
async function processBlogSourceFile({
blogSourceRelative,
contentPaths,
context,
options,
authorsMap,
}: {
blogSourceRelative: string;
contentPaths: BlogContentPaths;
context: LoadContext;
options: PluginOptions;
authorsMap?: AuthorsMap;
}): Promise<BlogPost | undefined> {
const {
siteConfig: {
baseUrl,
@ -231,21 +239,30 @@ async function processBlogSourceFile(
editUrl,
} = options;
// TODO remove this in favor of getLocalizedSource
// Lookup in localized folder in priority
const blogDirPath = await getFolderContainingFile(
getContentPathList(contentPaths),
blogSourceRelative,
);
const blogSourceAbsolute = path.join(blogDirPath, blogSourceRelative);
const {
source: blogSource,
// contentPath: blogDirPath
} = await getLocalizedSource({
relativeSource: blogSourceRelative,
contentPaths,
locale: context.i18n.currentLocale,
});
const {frontMatter, content, contentTitle, excerpt} =
await parseBlogPostMarkdownFile({
filePath: blogSourceAbsolute,
filePath: blogSource,
parseFrontMatter,
});
const aliasedSource = aliasedSitePath(blogSourceAbsolute, siteDir);
const aliasedSource = aliasedSitePath(blogSource, siteDir);
const draft = isDraft({frontMatter});
const unlisted = isUnlisted({frontMatter});
@ -274,14 +291,14 @@ async function processBlogSourceFile(
}
try {
const result = getFileCommitDate(blogSourceAbsolute, {
const result = getFileCommitDate(blogSource, {
age: 'oldest',
includeAuthor: false,
});
return result.date;
} catch (err) {
logger.warn(err);
return (await fs.stat(blogSourceAbsolute)).birthtime;
return (await fs.stat(blogSource)).birthtime;
}
}
@ -302,7 +319,7 @@ async function processBlogSourceFile(
function getBlogEditUrl() {
const blogPathRelative = path.relative(
blogDirPath,
path.resolve(blogSourceAbsolute),
path.resolve(blogSource),
);
if (typeof editUrl === 'function') {
@ -374,28 +391,36 @@ export async function generateBlogPosts(
return [];
}
const blogSourceFiles = await Globby(include, {
async function getBlogSourceFiles() {
const files = await Globby(include, {
cwd: contentPaths.contentPath,
ignore: exclude,
});
return filterFilesWithLocaleExtension({
files,
locales: context.i18n.locales,
});
}
const blogSourceFiles = await getBlogSourceFiles();
const authorsMap = await getAuthorsMap({
contentPaths,
authorsMapPath: options.authorsMapPath,
});
async function doProcessBlogSourceFile(blogSourceFile: string) {
async function doProcessBlogSourceFile(blogSourceRelative: string) {
try {
return await processBlogSourceFile(
blogSourceFile,
return await processBlogSourceFile({
blogSourceRelative,
contentPaths,
context,
options,
authorsMap,
);
});
} catch (err) {
throw new Error(
`Processing of blog source file path=${blogSourceFile} failed.`,
`Processing of blog source file path=${blogSourceRelative} failed.`,
{cause: err as Error},
);
}

View File

@ -72,6 +72,7 @@ yarn workspace v1.22.19image` is a collocated image path, this entry will be the
* preserved as-is. Default values will be applied when generating metadata
*/
export type BlogPostFrontMatter = {
// TODO Docusaurus v4: remove
/**
* @deprecated Use `slug` instead.
*/

View File

@ -11,6 +11,10 @@ module.exports = {
url: 'https://your-docusaurus-site.example.com',
baseUrl: '/',
favicon: 'img/favicon.ico',
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
},
markdown: {
parseFrontMatter: async (params) => {
const result = await params.defaultParseFrontMatter(params);

View File

@ -0,0 +1,22 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Head from '@docusaurus/Head';
export default class Home extends React.Component {
render() {
return (
<div>
<Head>
<title>translated Hello</title>
</Head>
<div>translated TypeScript...</div>
</div>
);
}
}

View File

@ -69,7 +69,7 @@ exports[`docusaurus-plugin-content-pages loads simple pages with french translat
},
{
"permalink": "/fr/typescript",
"source": "@site/src/pages/typescript.tsx",
"source": "@site/src/pages/typescript.fr.tsx",
"type": "jsx",
},
{

View File

@ -13,7 +13,6 @@ import {
aliasedSitePath,
docuHash,
getPluginI18nPath,
getFolderContainingFile,
addTrailingPathSeparator,
Globby,
createAbsoluteFilePathMatcher,
@ -22,6 +21,8 @@ import {
parseMarkdownFile,
isUnlisted,
isDraft,
getLocalizedSource,
filterFilesWithLocaleExtension,
} from '@docusaurus/utils';
import {validatePageFrontMatter} from './frontMatter';
@ -62,6 +63,17 @@ export default function pluginContentPages(
);
const dataDir = path.join(pluginDataDirRoot, options.id ?? DEFAULT_PLUGIN_ID);
async function getPageFiles() {
const files = await Globby(options.include, {
cwd: contentPaths.contentPath,
ignore: options.exclude,
});
return filterFilesWithLocaleExtension({
files,
locales: context.i18n.locales,
});
}
return {
name: 'docusaurus-plugin-content-pages',
@ -73,28 +85,21 @@ export default function pluginContentPages(
},
async loadContent() {
const {include} = options;
if (!(await fs.pathExists(contentPaths.contentPath))) {
return null;
}
const {baseUrl} = siteConfig;
const pagesFiles = await Globby(include, {
cwd: contentPaths.contentPath,
ignore: options.exclude,
});
const pagesFiles = await getPageFiles();
async function processPageSourceFile(
relativeSource: string,
): Promise<Metadata | undefined> {
// Lookup in localized folder in priority
const contentPath = await getFolderContainingFile(
getContentPathList(contentPaths),
const {source} = await getLocalizedSource({
relativeSource,
);
const source = path.join(contentPath, relativeSource);
contentPaths,
locale: context.i18n.currentLocale,
});
const aliasedSourcePath = aliasedSitePath(source, siteDir);
const permalink = normalizeUrl([
baseUrl,

View File

@ -5,7 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
export type PagesContentPaths = {
contentPath: string;
contentPathLocalized: string;
};
import type {ContentPaths} from '@docusaurus/utils';
export type PagesContentPaths = ContentPaths;

View File

@ -6,14 +6,17 @@
*/
import path from 'path';
import fs from 'fs-extra';
import _ from 'lodash';
import {DEFAULT_PLUGIN_ID} from './constants';
import {normalizeUrl} from './urlUtils';
import {findAsyncSequential} from './jsUtils';
import type {
TranslationFileContent,
TranslationFile,
I18n,
} from '@docusaurus/types';
import type {ContentPaths} from './markdownLinks';
/**
* Takes a list of translation file contents, and shallow-merges them into one.
@ -112,3 +115,112 @@ export function localizePath({
// Url paths; add a trailing slash so it's a valid base URL
return normalizeUrl([originalPath, i18n.currentLocale, '/']);
}
/**
* Localize a content file path
* ./dir/myDoc.md => ./dir/myDoc.fr.md
* @param filePath
* @param locale
*/
function addLocaleExtension(filePath: string, locale: string) {
const {name, dir, ext} = path.parse(filePath);
return path.join(dir, `${name}.${locale}${ext}`);
}
type LocalizedSource = {
contentPath: string;
source: string;
type: 'locale-extension' | 'locale-folder' | 'original';
};
function getLocalizedSourceCandidates({
relativeSource,
contentPaths,
locale,
}: {
relativeSource: string;
contentPaths: ContentPaths;
locale: string;
}): LocalizedSource[] {
// docs/myDoc.fr.md
const localeExtensionSource: LocalizedSource = {
contentPath: contentPaths.contentPath,
source: path.join(
contentPaths.contentPath,
addLocaleExtension(relativeSource, locale),
),
type: 'locale-extension',
};
// i18n/fr/docs/current/myDoc.md
const i18nFolderSource: LocalizedSource = {
contentPath: contentPaths.contentPath,
source: path.join(contentPaths.contentPathLocalized, relativeSource),
type: 'locale-folder',
};
// docs/myDoc.md
const originalSource: LocalizedSource = {
contentPath: contentPaths.contentPath,
source: path.join(contentPaths.contentPath, relativeSource),
type: 'original',
};
// Order matters
return [localeExtensionSource, i18nFolderSource, originalSource];
}
/**
* Returns the first existing localized path of a content file
* @param relativeSource
* @param contentPaths
* @param locale
*/
export async function getLocalizedSource({
relativeSource,
contentPaths,
locale,
}: {
relativeSource: string;
contentPaths: ContentPaths;
locale: string;
}): Promise<LocalizedSource> {
// docs/myDoc.fr.md
const candidates = getLocalizedSourceCandidates({
relativeSource,
contentPaths,
locale,
});
// TODO can we avoid/optimize this by passing all the files we know as param?
const localizedSource = await findAsyncSequential(candidates, (candidate) =>
fs.pathExists(candidate.source),
);
if (!localizedSource) {
throw new Error(
`Unexpected error, couldn't find any localized source for file at ${path.join(
contentPaths.contentPath,
relativeSource,
)}`,
);
}
return localizedSource;
}
export function filterFilesWithLocaleExtension({
files,
locales,
}: {
files: string[];
locales: string[];
}): string[] {
const possibleLocaleExtensions = new Set(
locales.map((locale) => `.${locale}`),
);
return files.filter((file) => {
const {name} = path.parse(file);
return !possibleLocaleExtensions.has(path.extname(name));
});
}

View File

@ -34,6 +34,8 @@ export {
updateTranslationFileMessages,
getPluginI18nPath,
localizePath,
getLocalizedSource,
filterFilesWithLocaleExtension,
} from './i18nUtils';
export {
removeSuffix,

View File

@ -0,0 +1,7 @@
---
date: 2024-01-03
---
# Blog i18n test
French version

View File

@ -0,0 +1,7 @@
---
date: 2024-01-03
---
# Blog i18n test
English version

View File

@ -0,0 +1,3 @@
# Docs i18n test
French version

View File

@ -0,0 +1,3 @@
# Docs i18n test
English version

View File

@ -0,0 +1,3 @@
# Page i18n test
French version

View File

@ -0,0 +1,3 @@
# Page i18n test
English version

View File

@ -26,6 +26,7 @@ import Readme from "../README.mdx"
### Other tests
- [React 18](/tests/pages/react-18)
- [i18n](/tests/pages/i18n)
- [Crash test](/tests/pages/crashTest)
- [Code block tests](/tests/pages/code-block-tests)
- [Link tests](/tests/pages/link-tests)

View File

@ -34,6 +34,7 @@ export const dogfoodingPluginInstances: PluginConfig[] = [
{
id: 'docs-tests',
routeBasePath: '/tests/docs',
editUrl: 'https://github.com/facebook/docusaurus/edit/main/website',
sidebarPath: '_dogfooding/docs-tests-sidebars.js',
versions: {
current: {
@ -69,8 +70,7 @@ export const dogfoodingPluginInstances: PluginConfig[] = [
id: 'blog-tests',
path: '_dogfooding/_blog tests',
routeBasePath: '/tests/blog',
editUrl:
'https://github.com/facebook/docusaurus/edit/main/website/_dogfooding/_blog-tests',
editUrl: 'https://github.com/facebook/docusaurus/edit/main/website',
postsPerPage: 3,
feedOptions: {
type: 'all',