diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index 3bbb5301bf92..cd80af445ba8 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -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 { +async function processBlogSourceFile({ + blogSourceRelative, + contentPaths, + context, + options, + authorsMap, +}: { + blogSourceRelative: string; + contentPaths: BlogContentPaths; + context: LoadContext; + options: PluginOptions; + authorsMap?: AuthorsMap; +}): Promise { 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, { - cwd: contentPaths.contentPath, - ignore: exclude, - }); + 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}, ); } diff --git a/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts b/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts index b1915f75196f..04ce1ba1d13b 100644 --- a/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts +++ b/packages/docusaurus-plugin-content-blog/src/plugin-content-blog.d.ts @@ -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. */ diff --git a/packages/docusaurus-plugin-content-pages/src/__tests__/__fixtures__/website/docusaurus.config.js b/packages/docusaurus-plugin-content-pages/src/__tests__/__fixtures__/website/docusaurus.config.js index d048d2caf5a1..b6b686f517e5 100644 --- a/packages/docusaurus-plugin-content-pages/src/__tests__/__fixtures__/website/docusaurus.config.js +++ b/packages/docusaurus-plugin-content-pages/src/__tests__/__fixtures__/website/docusaurus.config.js @@ -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); diff --git a/packages/docusaurus-plugin-content-pages/src/__tests__/__fixtures__/website/src/pages/typescript.fr.tsx b/packages/docusaurus-plugin-content-pages/src/__tests__/__fixtures__/website/src/pages/typescript.fr.tsx new file mode 100644 index 000000000000..96e86b6afd99 --- /dev/null +++ b/packages/docusaurus-plugin-content-pages/src/__tests__/__fixtures__/website/src/pages/typescript.fr.tsx @@ -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 ( +
+ + translated Hello + +
translated TypeScript...
+
+ ); + } +} diff --git a/packages/docusaurus-plugin-content-pages/src/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-plugin-content-pages/src/__tests__/__snapshots__/index.test.ts.snap index fc5fa2196778..f082b1028eec 100644 --- a/packages/docusaurus-plugin-content-pages/src/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-plugin-content-pages/src/__tests__/__snapshots__/index.test.ts.snap @@ -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", }, { diff --git a/packages/docusaurus-plugin-content-pages/src/index.ts b/packages/docusaurus-plugin-content-pages/src/index.ts index a4707110f2b4..cd9913e89108 100644 --- a/packages/docusaurus-plugin-content-pages/src/index.ts +++ b/packages/docusaurus-plugin-content-pages/src/index.ts @@ -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 { - // 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, diff --git a/packages/docusaurus-plugin-content-pages/src/types.ts b/packages/docusaurus-plugin-content-pages/src/types.ts index 2d11492cb04e..cec39777a595 100644 --- a/packages/docusaurus-plugin-content-pages/src/types.ts +++ b/packages/docusaurus-plugin-content-pages/src/types.ts @@ -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; diff --git a/packages/docusaurus-utils/src/i18nUtils.ts b/packages/docusaurus-utils/src/i18nUtils.ts index 15723648d72a..7b8c458b61ac 100644 --- a/packages/docusaurus-utils/src/i18nUtils.ts +++ b/packages/docusaurus-utils/src/i18nUtils.ts @@ -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 { + // 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)); + }); +} diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 6db01244d006..9e772c643286 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -34,6 +34,8 @@ export { updateTranslationFileMessages, getPluginI18nPath, localizePath, + getLocalizedSource, + filterFilesWithLocaleExtension, } from './i18nUtils'; export { removeSuffix, diff --git a/website/_dogfooding/_blog tests/i18n/index.fr.md b/website/_dogfooding/_blog tests/i18n/index.fr.md new file mode 100644 index 000000000000..f948b507458b --- /dev/null +++ b/website/_dogfooding/_blog tests/i18n/index.fr.md @@ -0,0 +1,7 @@ +--- +date: 2024-01-03 +--- + +# Blog i18n test + +French version diff --git a/website/_dogfooding/_blog tests/i18n/index.md b/website/_dogfooding/_blog tests/i18n/index.md new file mode 100644 index 000000000000..06fcf1dafdfb --- /dev/null +++ b/website/_dogfooding/_blog tests/i18n/index.md @@ -0,0 +1,7 @@ +--- +date: 2024-01-03 +--- + +# Blog i18n test + +English version diff --git a/website/_dogfooding/_docs tests/tests/i18n/index.fr.md b/website/_dogfooding/_docs tests/tests/i18n/index.fr.md new file mode 100644 index 000000000000..eed021b827b1 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/i18n/index.fr.md @@ -0,0 +1,3 @@ +# Docs i18n test + +French version diff --git a/website/_dogfooding/_docs tests/tests/i18n/index.md b/website/_dogfooding/_docs tests/tests/i18n/index.md new file mode 100644 index 000000000000..e108fbddee42 --- /dev/null +++ b/website/_dogfooding/_docs tests/tests/i18n/index.md @@ -0,0 +1,3 @@ +# Docs i18n test + +English version diff --git a/website/_dogfooding/_pages tests/i18n/index.fr.md b/website/_dogfooding/_pages tests/i18n/index.fr.md new file mode 100644 index 000000000000..4917f6b55fdd --- /dev/null +++ b/website/_dogfooding/_pages tests/i18n/index.fr.md @@ -0,0 +1,3 @@ +# Page i18n test + +French version diff --git a/website/_dogfooding/_pages tests/i18n/index.md b/website/_dogfooding/_pages tests/i18n/index.md new file mode 100644 index 000000000000..3d23cbe7b921 --- /dev/null +++ b/website/_dogfooding/_pages tests/i18n/index.md @@ -0,0 +1,3 @@ +# Page i18n test + +English version diff --git a/website/_dogfooding/_pages tests/index.mdx b/website/_dogfooding/_pages tests/index.mdx index 4f7c2889bff1..851eebb03835 100644 --- a/website/_dogfooding/_pages tests/index.mdx +++ b/website/_dogfooding/_pages tests/index.mdx @@ -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) diff --git a/website/_dogfooding/dogfooding.config.ts b/website/_dogfooding/dogfooding.config.ts index 3e57c13c36ad..1cb8c7a27af4 100644 --- a/website/_dogfooding/dogfooding.config.ts +++ b/website/_dogfooding/dogfooding.config.ts @@ -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',