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

View File

@ -11,6 +11,10 @@ module.exports = {
url: 'https://your-docusaurus-site.example.com', url: 'https://your-docusaurus-site.example.com',
baseUrl: '/', baseUrl: '/',
favicon: 'img/favicon.ico', favicon: 'img/favicon.ico',
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
},
markdown: { markdown: {
parseFrontMatter: async (params) => { parseFrontMatter: async (params) => {
const result = await params.defaultParseFrontMatter(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", "permalink": "/fr/typescript",
"source": "@site/src/pages/typescript.tsx", "source": "@site/src/pages/typescript.fr.tsx",
"type": "jsx", "type": "jsx",
}, },
{ {

View File

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

View File

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

View File

@ -6,14 +6,17 @@
*/ */
import path from 'path'; import path from 'path';
import fs from 'fs-extra';
import _ from 'lodash'; import _ from 'lodash';
import {DEFAULT_PLUGIN_ID} from './constants'; import {DEFAULT_PLUGIN_ID} from './constants';
import {normalizeUrl} from './urlUtils'; import {normalizeUrl} from './urlUtils';
import {findAsyncSequential} from './jsUtils';
import type { import type {
TranslationFileContent, TranslationFileContent,
TranslationFile, TranslationFile,
I18n, I18n,
} from '@docusaurus/types'; } from '@docusaurus/types';
import type {ContentPaths} from './markdownLinks';
/** /**
* Takes a list of translation file contents, and shallow-merges them into one. * 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 // Url paths; add a trailing slash so it's a valid base URL
return normalizeUrl([originalPath, i18n.currentLocale, '/']); 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, updateTranslationFileMessages,
getPluginI18nPath, getPluginI18nPath,
localizePath, localizePath,
getLocalizedSource,
filterFilesWithLocaleExtension,
} from './i18nUtils'; } from './i18nUtils';
export { export {
removeSuffix, 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 ### Other tests
- [React 18](/tests/pages/react-18) - [React 18](/tests/pages/react-18)
- [i18n](/tests/pages/i18n)
- [Crash test](/tests/pages/crashTest) - [Crash test](/tests/pages/crashTest)
- [Code block tests](/tests/pages/code-block-tests) - [Code block tests](/tests/pages/code-block-tests)
- [Link tests](/tests/pages/link-tests) - [Link tests](/tests/pages/link-tests)

View File

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