mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-17 02:56:57 +02:00
feat(v2): core v2 i18n support + Docusaurus site Crowdin integration (#3325)
* docs i18n initial poc * docs i18n initial poc * docs i18n initial poc * docs i18n initial poc * crowdin-v2 attempt * fix source * use crowdin env variable * try to install crowdin on netlify * try to install crowdin on netlify * try to use crowdin jar directly * try to curl the crowdin jar * add java version cmd * try to run crowdin jar in netlify * fix translatedDocsDirPath * fix loadContext issue due to site baseUrl not being modified in generted config file * real validateLocalesFile * add locale option to deploy command * better LocalizationFile type * create util getPluginI18nPath * better core localization context loading code * More explicit VersionMetadata type for localized docs folders * Ability to translate blog posts with Crowdin! * blog: refactor markdown loader + report broken links + try to get linkify working better * upgrade crowdin config to upload all docs folder files except source code related files * try to support translated pages * make markdown pages translation work * add write-translations cli command template * fix site not reloaded with correct options * refactor a bit the read/write of @generated/i18n.json file * Add <Translate> + translate() API + use it on the docusaurus homepage * watch locale translation dir * early POC of adding babel parsing for translation extraction * fs.stat => pathExists * add install:fast script * TSC: noUnusedLocals false as it's already checked by eslint * POC of extracting translations from source code * minor typo * fix extracted key to code * initial docs extracted translations * stable plugin translations POC * add crowdin commands * quickfix for i18n deployment * POC of themeConfig translation * add ability to have localized site without path prefix * sidebar typo * refactor translation system to output multiple translation files * translate properly the docs plugin * improve theme classic translation * rework translation extractor to handle new Chrome I18n JSON format (include id/description) * writeTranslations: allow to pass locales cli arg * fix ThemeConfig TS issues * fix localizePath errors * temporary add write-translations to netlify deploy preview * complete example of french translated folder * update fr folder * remove all translations from repo * minor translation refactors * fix all docs-related tests * fix blog feed tests * fix last blog tests * refactor i18n context a bit, extract codeTranslations in an extra generated file * improve @generated/i18n type * fix some i18n todos * minor refactor * fix logo typing issue after merge * move i18n.json to siteConfig instead * try to fix windows CI build * fix config test * attempt to fix windows non-posix path * increase v1 minify css jest timeout due to flaky test * proper support for localizePath on windows * remove non-functional install:fast * docs, fix docsDirPathLocalized * fix Docs i18n / md linkify issues * ensure theme-classic swizzling will use "nextjs" sources (transpiled less aggressively, to make them human readable) * fix some snapshots * improve themeConfig translation code * refactor a bit getPluginI18nPath * readTranslationFileContent => ensure files are valid, fail fast * fix versions tests * add extractSourceCodeAstTranslations comments/resource links * ignore eslint: packages/docusaurus-theme-classic/lib-next/ * fix windows CI with cross-env * crowdin ignore .DS_Store * improve writeTranslations + add exhaustive tests for translations.ts * remove typo * Wire currentLocale to algolia search * improve i18n locale error * Add tests for translationsExtractor.ts * better code translation extraction regarding statically evaluable code * fix typo * fix typo * improve theme-classic transpilation * refactor + add i18n tests * typo * test new utils * add missing snapshots * fix snapshot * blog onBrokenMarkdownLink * add sidebars tests * theme-classic index should now use ES modules * tests for theme-classic translations * useless comment * add more translation tests * simplify/cleanup writeTranslations * try to fix Netlify fr deployment * blog: test translated md is used during feed generation * blog: better i18n tests regarding editUrl + md translation application * more i18n tests for docs plugin * more i18n tests for docs plugin * Add tests for pages i18n * polish docusaurus build i18n logs
This commit is contained in:
parent
85fe96d112
commit
3166fab307
107 changed files with 5447 additions and 649 deletions
|
@ -25,6 +25,7 @@ packages/docusaurus-plugin-ideal-image/lib/
|
|||
packages/docusaurus-plugin-ideal-image/copyUntypedFiles.js
|
||||
packages/docusaurus-theme-common/lib/
|
||||
packages/docusaurus-theme-classic/lib/
|
||||
packages/docusaurus-theme-classic/lib-next/
|
||||
packages/docusaurus-theme-bootstrap/lib/
|
||||
packages/docusaurus-migrate/lib/
|
||||
|
||||
|
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -29,11 +29,16 @@ packages/docusaurus-plugin-sitemap/lib/
|
|||
packages/docusaurus-plugin-ideal-image/lib/
|
||||
packages/docusaurus-theme-common/lib/
|
||||
packages/docusaurus-theme-classic/lib/
|
||||
packages/docusaurus-theme-classic/lib-next/
|
||||
packages/docusaurus-theme-bootstrap/lib/
|
||||
packages/docusaurus-migrate/lib/
|
||||
|
||||
website/netlifyDeployPreview
|
||||
website/netlifyDeployPreview/*
|
||||
!website/netlifyDeployPreview/index.html
|
||||
!website/netlifyDeployPreview/_redirects
|
||||
|
||||
website-1.x-migrated
|
||||
|
||||
website/i18n/**/*
|
||||
#!website/i18n/fr
|
||||
#!website/i18n/fr/**/*
|
||||
|
|
155
crowdin-v2.yaml
Normal file
155
crowdin-v2.yaml
Normal file
|
@ -0,0 +1,155 @@
|
|||
#
|
||||
# Your Crowdin credentials
|
||||
#
|
||||
'project_id': '416738'
|
||||
'api_token_env': 'CROWDIN_PERSONAL_TOKEN'
|
||||
'base_path': '.'
|
||||
'base_url': 'https://api.crowdin.com'
|
||||
|
||||
#
|
||||
# Choose file structure in Crowdin
|
||||
# e.g. true or false
|
||||
#
|
||||
'preserve_hierarchy': true
|
||||
|
||||
#
|
||||
# Files configuration
|
||||
#
|
||||
files:
|
||||
[
|
||||
{
|
||||
'source': '/website/i18n/en/**/*',
|
||||
'translation': '/website/i18n/%two_letters_code%/**/%original_file_name%',
|
||||
'ignore': ['/**/.DS_Store'],
|
||||
},
|
||||
{
|
||||
'source': '/website/docs/**/*',
|
||||
'translation': '/website/i18n/%two_letters_code%/docusaurus-plugin-content-docs/current/**/%original_file_name%',
|
||||
'ignore': ['/**/.DS_Store'],
|
||||
},
|
||||
{
|
||||
'source': '/website/community/**/*',
|
||||
'translation': '/website/i18n/%two_letters_code%/docusaurus-plugin-content-docs-community/current/**/%original_file_name%',
|
||||
'ignore': ['/**/.DS_Store'],
|
||||
},
|
||||
{
|
||||
'source': '/website/versioned_docs/**/*',
|
||||
'translation': '/website/i18n/%two_letters_code%/docusaurus-plugin-content-docs/**/%original_file_name%',
|
||||
'ignore': ['/**/.DS_Store'],
|
||||
},
|
||||
{
|
||||
'source': '/website-1.x/blog/**/*',
|
||||
'translation': '/website/i18n/%two_letters_code%/docusaurus-plugin-content-blog/**/%original_file_name%',
|
||||
'ignore': ['/**/.DS_Store'],
|
||||
},
|
||||
{
|
||||
'source': '/website/src/pages/**/*',
|
||||
'translation': '/website/i18n/%two_letters_code%/docusaurus-plugin-content-pages/**/%original_file_name%',
|
||||
'ignore':
|
||||
[
|
||||
'/**/*.js',
|
||||
'/**/*.jsx',
|
||||
'/**/*.ts',
|
||||
'/**/*.tsx',
|
||||
'/**/*.css',
|
||||
'/**/.DS_Store',
|
||||
],
|
||||
},
|
||||
]
|
||||
#
|
||||
# Source files filter
|
||||
# e.g. "/resources/en/*.json"
|
||||
#
|
||||
#"source" : "/website/docs/**/*.md",
|
||||
#
|
||||
# Where translations will be placed
|
||||
# e.g. "/resources/docs/%two_letters_code%/%original_file_name%"
|
||||
#
|
||||
#"translation" : "/website/i18n/%language%/docs/current/%original_file_name%",
|
||||
#
|
||||
# Files or directories for ignore
|
||||
# e.g. ["/**/?.txt", "/**/[0-9].txt", "/**/*\?*.txt"]
|
||||
#
|
||||
#"ignore" : [],
|
||||
#
|
||||
# The dest allows you to specify a file name in Crowdin
|
||||
# e.g. "/messages.json"
|
||||
#
|
||||
#"dest" : "",
|
||||
#
|
||||
# File type
|
||||
# e.g. "json"
|
||||
#
|
||||
#"type" : "",
|
||||
#
|
||||
# The parameter "update_option" is optional. If it is not set, after the files update the translations for changed strings will be removed. Use to fix typos and for minor changes in the source strings
|
||||
# e.g. "update_as_unapproved" or "update_without_changes"
|
||||
#
|
||||
#"update_option" : "",
|
||||
#
|
||||
# Start block (for XML only)
|
||||
#
|
||||
#
|
||||
# Defines whether to translate tags attributes.
|
||||
# e.g. 0 or 1 (Default is 1)
|
||||
#
|
||||
# "translate_attributes" : 1,
|
||||
#
|
||||
# Defines whether to translate texts placed inside the tags.
|
||||
# e.g. 0 or 1 (Default is 1)
|
||||
#
|
||||
# "translate_content" : 1,
|
||||
#
|
||||
# This is an array of strings, where each item is the XPaths to DOM element that should be imported
|
||||
# e.g. ["/content/text", "/content/text[@value]"]
|
||||
#
|
||||
# "translatable_elements" : [],
|
||||
#
|
||||
# Defines whether to split long texts into smaller text segments
|
||||
# e.g. 0 or 1 (Default is 1)
|
||||
#
|
||||
# "content_segmentation" : 1,
|
||||
#
|
||||
# End block (for XML only)
|
||||
#
|
||||
#
|
||||
# Start .properties block
|
||||
#
|
||||
#
|
||||
# Defines whether single quote should be escaped by another single quote or backslash in exported translations
|
||||
# e.g. 0 or 1 or 2 or 3 (Default is 3)
|
||||
# 0 - do not escape single quote;
|
||||
# 1 - escape single quote by another single quote;
|
||||
# 2 - escape single quote by backslash;
|
||||
# 3 - escape single quote by another single quote only in strings containing variables ( {0} ).
|
||||
#
|
||||
# "escape_quotes" : 3,
|
||||
#
|
||||
# Defines whether any special characters (=, :, ! and #) should be escaped by backslash in exported translations.
|
||||
# e.g. 0 or 1 (Default is 0)
|
||||
# 0 - do not escape special characters
|
||||
# 1 - escape special characters by a backslash
|
||||
#
|
||||
# "escape_special_characters": 0
|
||||
#
|
||||
#
|
||||
# End .properties block
|
||||
#
|
||||
#
|
||||
# Often software projects have custom names for the directories where translations are placed. crowdin-cli allows you to map your own languages to be understandable by Crowdin.
|
||||
#
|
||||
#"languages_mapping" : {
|
||||
# "two_letters_code" : {
|
||||
# "crowdin_language_code" : "local_name"
|
||||
# }
|
||||
#},
|
||||
#
|
||||
# Does the first line contain header?
|
||||
# e.g. true or false
|
||||
#
|
||||
#"first_line_contains_header" : true,
|
||||
#
|
||||
# for spreadsheets
|
||||
# e.g. "identifier,source_phrase,context,uk,ru,fr"
|
||||
#
|
||||
# "scheme" : "",
|
|
@ -29,9 +29,9 @@ To serve as an example page when styling markdown based Docusaurus sites.
|
|||
|
||||
## Emphasis
|
||||
|
||||
Emphasis, aka italics, with *asterisks* or _underscores_.
|
||||
Emphasis, aka italics, with _asterisks_ or _underscores_.
|
||||
|
||||
Strong emphasis, aka bold, with **asterisks** or __underscores__.
|
||||
Strong emphasis, aka bold, with **asterisks** or **underscores**.
|
||||
|
||||
Combined emphasis with **asterisks and _underscores_**.
|
||||
|
||||
|
@ -48,11 +48,11 @@ Strikethrough uses two tildes. ~~Scratch this.~~
|
|||
1. Ordered sub-list
|
||||
1. And another item.
|
||||
|
||||
* Unordered list can use asterisks
|
||||
- Unordered list can use asterisks
|
||||
|
||||
- Or minuses
|
||||
* Or minuses
|
||||
|
||||
+ Or pluses
|
||||
- Or pluses
|
||||
|
||||
---
|
||||
|
||||
|
@ -92,7 +92,6 @@ Images from any folder can be used by providing path to file. Path should be rel
|
|||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
## Code
|
||||
|
|
|
@ -29,9 +29,9 @@ To serve as an example page when styling markdown based Docusaurus sites.
|
|||
|
||||
## Emphasis
|
||||
|
||||
Emphasis, aka italics, with *asterisks* or _underscores_.
|
||||
Emphasis, aka italics, with _asterisks_ or _underscores_.
|
||||
|
||||
Strong emphasis, aka bold, with **asterisks** or __underscores__.
|
||||
Strong emphasis, aka bold, with **asterisks** or **underscores**.
|
||||
|
||||
Combined emphasis with **asterisks and _underscores_**.
|
||||
|
||||
|
@ -48,11 +48,11 @@ Strikethrough uses two tildes. ~~Scratch this.~~
|
|||
1. Ordered sub-list
|
||||
1. And another item.
|
||||
|
||||
* Unordered list can use asterisks
|
||||
- Unordered list can use asterisks
|
||||
|
||||
- Or minuses
|
||||
* Or minuses
|
||||
|
||||
+ Or pluses
|
||||
- Or pluses
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -29,9 +29,9 @@ To serve as an example page when styling markdown based Docusaurus sites.
|
|||
|
||||
## Emphasis
|
||||
|
||||
Emphasis, aka italics, with *asterisks* or _underscores_.
|
||||
Emphasis, aka italics, with _asterisks_ or _underscores_.
|
||||
|
||||
Strong emphasis, aka bold, with **asterisks** or __underscores__.
|
||||
Strong emphasis, aka bold, with **asterisks** or **underscores**.
|
||||
|
||||
Combined emphasis with **asterisks and _underscores_**.
|
||||
|
||||
|
@ -48,11 +48,11 @@ Strikethrough uses two tildes. ~~Scratch this.~~
|
|||
1. Ordered sub-list
|
||||
1. And another item.
|
||||
|
||||
* Unordered list can use asterisks
|
||||
- Unordered list can use asterisks
|
||||
|
||||
- Or minuses
|
||||
* Or minuses
|
||||
|
||||
+ Or pluses
|
||||
- Or pluses
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@ const ignorePatterns = [
|
|||
'/packages/docusaurus-plugin-content-blog/lib',
|
||||
'/packages/docusaurus-plugin-content-docs/lib',
|
||||
'/packages/docusaurus-plugin-content-pages/lib',
|
||||
'/packages/docusaurus-theme-classic/lib',
|
||||
'/packages/docusaurus-theme-classic/lib-next',
|
||||
'/packages/docusaurus-migrate/lib',
|
||||
];
|
||||
|
||||
|
|
|
@ -29,6 +29,9 @@
|
|||
"serve:v2:ssl:gencert": "openssl req -x509 -nodes -days 365 -newkey rsa:4096 -subj \"/C=US/ST=Docusaurus/L=Anywhere/O=Dis/CN=localhost\" -keyout ./website/.docusaurus/selfsigned.key -out ./website/.docusaurus/selfsigned.crt",
|
||||
"serve:v2:ssl:message": "echo '\n\n\nServing Docusaurus with HTTPS on localhost requires to disable the Chrome security: chrome://flags/#allow-insecure-localhost\n\n\n'",
|
||||
"serve:v2:ssl:serve": "serve website/build --ssl-cert ./website/.docusaurus/selfsigned.crt --ssl-key ./website/.docusaurus/selfsigned.key",
|
||||
"crowdin:upload:v2": "crowdin upload sources --config ./crowdin-v2.yaml",
|
||||
"crowdin:uploadTranslations:v2": "crowdin upload translations --config ./crowdin-v2.yaml",
|
||||
"crowdin:download:v2": "crowdin download --config ./crowdin-v2.yaml",
|
||||
"changelog": "lerna-changelog",
|
||||
"postinstall": "yarn lock:update && yarn build:packages",
|
||||
"prettier": "prettier --config .prettierrc --write \"**/*.{js,ts}\"",
|
||||
|
@ -60,6 +63,7 @@
|
|||
"@babel/core": "^7.12.3",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.12.1",
|
||||
"@babel/preset-typescript": "^7.12.1",
|
||||
"@types/express": "^4.17.2",
|
||||
"@types/fs-extra": "^9.0.4",
|
||||
|
@ -92,6 +96,7 @@
|
|||
"@typescript-eslint/parser": "^4.8.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"concurrently": "^5.2.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"enzyme": "^3.10.0",
|
||||
"enzyme-adapter-react-16": "^1.15.1",
|
||||
"eslint": "^7.13.0",
|
||||
|
|
|
@ -40,7 +40,7 @@ describe('server utils', () => {
|
|||
expect(css).toMatchSnapshot();
|
||||
|
||||
await expect(utils.minifyCss(notCss)).rejects.toMatchSnapshot();
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
test('autoprefix css', async () => {
|
||||
const testCss = fs.readFileSync(
|
||||
|
|
|
@ -47,6 +47,20 @@ declare module '@generated/globalData' {
|
|||
export default globalData;
|
||||
}
|
||||
|
||||
declare module '@generated/i18n' {
|
||||
const i18n: {
|
||||
defaultLocale: string;
|
||||
locales: [string, ...string[]];
|
||||
currentLocale: string;
|
||||
};
|
||||
export default i18n;
|
||||
}
|
||||
|
||||
declare module '@generated/codeTranslations' {
|
||||
const codeTranslations: Record<string, string>;
|
||||
export default codeTranslations;
|
||||
}
|
||||
|
||||
declare module '@theme/*';
|
||||
|
||||
declare module '@theme-original/*';
|
||||
|
@ -69,6 +83,18 @@ declare module '@docusaurus/Link' {
|
|||
export default Link;
|
||||
}
|
||||
|
||||
declare module '@docusaurus/Translate' {
|
||||
type TranslateProps = {children: string; id?: string; description?: string};
|
||||
const Translate: (props: TranslateProps) => JSX.Element;
|
||||
export default Translate;
|
||||
|
||||
export function translate(param: {
|
||||
message: string;
|
||||
id?: string;
|
||||
description?: string;
|
||||
}): string;
|
||||
}
|
||||
|
||||
declare module '@docusaurus/router' {
|
||||
export const Redirect: (props: {to: string}) => import('react').Component;
|
||||
export function matchPath(
|
||||
|
|
|
@ -14,7 +14,7 @@ import writeRedirectFiles, {
|
|||
toRedirectFilesMetadata,
|
||||
RedirectFileMetadata,
|
||||
} from './writeRedirectFiles';
|
||||
import {removePrefix} from '@docusaurus/utils';
|
||||
import {removePrefix, addLeadingSlash} from '@docusaurus/utils';
|
||||
|
||||
export default function pluginClientRedirectsPages(
|
||||
_context: LoadContext,
|
||||
|
@ -27,7 +27,7 @@ export default function pluginClientRedirectsPages(
|
|||
async postBuild(props: Props) {
|
||||
const pluginContext: PluginContext = {
|
||||
relativeRoutesPaths: props.routesPaths.map(
|
||||
(path) => `/${removePrefix(path, props.baseUrl)}`,
|
||||
(path) => `${addLeadingSlash(removePrefix(path, props.baseUrl))}`,
|
||||
),
|
||||
baseUrl: props.baseUrl,
|
||||
outDir: props.outDir,
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: This post links to another one!
|
||||
---
|
||||
|
||||
[Good link 1](2018-12-14-Happy-First-Birthday-Slash.md)
|
||||
|
||||
[Good link 2](./2018-12-14-Happy-First-Birthday-Slash.md)
|
||||
|
||||
[Bad link 1](postNotExist1.md)
|
||||
|
||||
[Bad link 1](./postNotExist2.mdx)
|
|
@ -2,4 +2,4 @@
|
|||
title: Happy 1st Birthday Slash!
|
||||
---
|
||||
|
||||
pattern name
|
||||
Happy birthday!
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Happy 1st Birthday Slash! (translated)
|
||||
---
|
||||
|
||||
Happy birthday! (translated)
|
|
@ -40,11 +40,11 @@ exports[`blogFeed atom shows feed item for each post 1`] = `
|
|||
<summary type=\\"html\\"><![CDATA[date inside front matter]]></summary>
|
||||
</entry>
|
||||
<entry>
|
||||
<title type=\\"html\\"><![CDATA[Happy 1st Birthday Slash!]]></title>
|
||||
<id>Happy 1st Birthday Slash!</id>
|
||||
<title type=\\"html\\"><![CDATA[Happy 1st Birthday Slash! (translated)]]></title>
|
||||
<id>Happy 1st Birthday Slash! (translated)</id>
|
||||
<link href=\\"https://docusaurus.io/blog/2018/12/14/Happy-First-Birthday-Slash\\"/>
|
||||
<updated>2018-12-14T00:00:00.000Z</updated>
|
||||
<summary type=\\"html\\"><![CDATA[pattern name]]></summary>
|
||||
<summary type=\\"html\\"><![CDATA[Happy birthday! (translated)]]></summary>
|
||||
</entry>
|
||||
</feed>"
|
||||
`;
|
||||
|
@ -90,11 +90,11 @@ exports[`blogFeed rss shows feed item for each post 1`] = `
|
|||
<description><![CDATA[date inside front matter]]></description>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Happy 1st Birthday Slash!]]></title>
|
||||
<title><![CDATA[Happy 1st Birthday Slash! (translated)]]></title>
|
||||
<link>https://docusaurus.io/blog/2018/12/14/Happy-First-Birthday-Slash</link>
|
||||
<guid>Happy 1st Birthday Slash!</guid>
|
||||
<guid>Happy 1st Birthday Slash! (translated)</guid>
|
||||
<pubDate>Fri, 14 Dec 2018 00:00:00 GMT</pubDate>
|
||||
<description><![CDATA[pattern name]]></description>
|
||||
<description><![CDATA[Happy birthday! (translated)]]></description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>"
|
||||
|
|
|
@ -1,5 +1,20 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`report broken markdown links 1`] = `
|
||||
"---
|
||||
title: This post links to another one!
|
||||
---
|
||||
|
||||
[Good link 1](/blog/2018/12/14/Happy-First-Birthday-Slash)
|
||||
|
||||
[Good link 2](/blog/2018/12/14/Happy-First-Birthday-Slash)
|
||||
|
||||
[Bad link 1](postNotExist1.md)
|
||||
|
||||
[Bad link 1](./postNotExist2.mdx)
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`transform to correct link 1`] = `
|
||||
"---
|
||||
title: This post links to another one!
|
||||
|
|
|
@ -8,12 +8,25 @@
|
|||
import path from 'path';
|
||||
import {generateBlogFeed} from '../blogUtils';
|
||||
import {LoadContext} from '@docusaurus/types';
|
||||
import {PluginOptions} from '../types';
|
||||
import {PluginOptions, BlogContentPaths} from '../types';
|
||||
|
||||
function getBlogContentPaths(siteDir: string): BlogContentPaths {
|
||||
return {
|
||||
contentPath: path.resolve(siteDir, 'blog'),
|
||||
contentPathLocalized: path.resolve(
|
||||
siteDir,
|
||||
'i18n',
|
||||
'en',
|
||||
'docusaurus-plugin-content-blog',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
describe('blogFeed', () => {
|
||||
['atom', 'rss'].forEach((feedType) => {
|
||||
(['atom', 'rss'] as const).forEach((feedType) => {
|
||||
describe(`${feedType}`, () => {
|
||||
test('can show feed without posts', async () => {
|
||||
const siteDir = __dirname;
|
||||
const siteConfig = {
|
||||
title: 'Hello',
|
||||
baseUrl: '/',
|
||||
|
@ -22,8 +35,9 @@ describe('blogFeed', () => {
|
|||
};
|
||||
|
||||
const feed = await generateBlogFeed(
|
||||
getBlogContentPaths(siteDir),
|
||||
{
|
||||
siteDir: __dirname,
|
||||
siteDir,
|
||||
siteConfig,
|
||||
} as LoadContext,
|
||||
{
|
||||
|
@ -31,7 +45,7 @@ describe('blogFeed', () => {
|
|||
routeBasePath: 'blog',
|
||||
include: ['*.md', '*.mdx'],
|
||||
feedOptions: {
|
||||
type: feedType,
|
||||
type: [feedType],
|
||||
copyright: 'Copyright',
|
||||
},
|
||||
} as PluginOptions,
|
||||
|
@ -52,6 +66,7 @@ describe('blogFeed', () => {
|
|||
};
|
||||
|
||||
const feed = await generateBlogFeed(
|
||||
getBlogContentPaths(siteDir),
|
||||
{
|
||||
siteDir,
|
||||
siteConfig,
|
||||
|
@ -62,7 +77,7 @@ describe('blogFeed', () => {
|
|||
routeBasePath: 'blog',
|
||||
include: ['*r*.md', '*.mdx'], // skip no-date.md - it won't play nice with snapshots
|
||||
feedOptions: {
|
||||
type: feedType,
|
||||
type: [feedType],
|
||||
copyright: 'Copyright',
|
||||
},
|
||||
} as PluginOptions,
|
||||
|
|
|
@ -8,9 +8,15 @@
|
|||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import pluginContentBlog from '../index';
|
||||
import {DocusaurusConfig, LoadContext} from '@docusaurus/types';
|
||||
import {DocusaurusConfig, LoadContext, I18n} from '@docusaurus/types';
|
||||
import {PluginOptionSchema} from '../pluginOptionSchema';
|
||||
|
||||
const DefaultI18N: I18n = {
|
||||
currentLocale: 'en',
|
||||
locales: ['en'],
|
||||
defaultLocale: 'en',
|
||||
};
|
||||
|
||||
function validateAndNormalize(schema, options) {
|
||||
const {value, error} = schema.validate(options);
|
||||
if (error) {
|
||||
|
@ -34,6 +40,7 @@ describe('loadBlog', () => {
|
|||
siteDir,
|
||||
siteConfig,
|
||||
generatedFilesDir,
|
||||
i18n: DefaultI18N,
|
||||
} as LoadContext,
|
||||
validateAndNormalize(PluginOptionSchema, {
|
||||
path: pluginPath,
|
||||
|
@ -66,26 +73,28 @@ describe('loadBlog', () => {
|
|||
tags: [],
|
||||
nextItem: {
|
||||
permalink: '/blog/2018/12/14/Happy-First-Birthday-Slash',
|
||||
title: 'Happy 1st Birthday Slash!',
|
||||
title: 'Happy 1st Birthday Slash! (translated)',
|
||||
},
|
||||
truncated: false,
|
||||
});
|
||||
|
||||
expect(
|
||||
blogPosts.find((v) => v.metadata.title === 'Happy 1st Birthday Slash!')
|
||||
.metadata,
|
||||
blogPosts.find(
|
||||
(v) => v.metadata.title === 'Happy 1st Birthday Slash! (translated)',
|
||||
).metadata,
|
||||
).toEqual({
|
||||
editUrl:
|
||||
'https://github.com/facebook/docusaurus/edit/master/website-1x/blog/2018-12-14-Happy-First-Birthday-Slash.md',
|
||||
'https://github.com/facebook/docusaurus/edit/master/website-1x/i18n/en/docusaurus-plugin-content-blog/2018-12-14-Happy-First-Birthday-Slash.md',
|
||||
permalink: '/blog/2018/12/14/Happy-First-Birthday-Slash',
|
||||
readingTime: 0.01,
|
||||
readingTime: 0.015,
|
||||
source: path.join(
|
||||
'@site',
|
||||
pluginPath,
|
||||
// pluginPath,
|
||||
path.join('i18n', 'en', 'docusaurus-plugin-content-blog'),
|
||||
'2018-12-14-Happy-First-Birthday-Slash.md',
|
||||
),
|
||||
title: 'Happy 1st Birthday Slash!',
|
||||
description: `pattern name`,
|
||||
title: 'Happy 1st Birthday Slash! (translated)',
|
||||
description: `Happy birthday! (translated)`,
|
||||
date: new Date('2018-12-14'),
|
||||
tags: [],
|
||||
prevItem: {
|
||||
|
|
|
@ -7,11 +7,14 @@
|
|||
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import {linkify} from '../blogUtils';
|
||||
import {BlogPost} from '../types';
|
||||
import {linkify, LinkifyParams} from '../blogUtils';
|
||||
import {BlogBrokenMarkdownLink, BlogContentPaths, BlogPost} from '../types';
|
||||
|
||||
const sitePath = path.join(__dirname, '__fixtures__', 'website');
|
||||
const blogPath = path.join(sitePath, 'blog-with-ref');
|
||||
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
||||
const contentPaths: BlogContentPaths = {
|
||||
contentPath: path.join(siteDir, 'blog-with-ref'),
|
||||
contentPathLocalized: path.join(siteDir, 'blog-with-ref-localized'),
|
||||
};
|
||||
const pluginDir = 'blog-with-ref';
|
||||
const blogPosts: BlogPost[] = [
|
||||
{
|
||||
|
@ -36,14 +39,26 @@ const blogPosts: BlogPost[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const transform = (filepath: string) => {
|
||||
const content = fs.readFileSync(filepath, 'utf-8');
|
||||
const transformedContent = linkify(content, sitePath, blogPath, blogPosts);
|
||||
return [content, transformedContent];
|
||||
const transform = (filePath: string, options?: Partial<LinkifyParams>) => {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
||||
const transformedContent = linkify({
|
||||
filePath,
|
||||
fileContent,
|
||||
siteDir,
|
||||
contentPaths,
|
||||
blogPosts,
|
||||
onBrokenMarkdownLink: (brokenMarkdownLink) => {
|
||||
throw new Error(
|
||||
`Broken markdown link found: ${JSON.stringify(brokenMarkdownLink)}`,
|
||||
);
|
||||
},
|
||||
...options,
|
||||
});
|
||||
return [fileContent, transformedContent];
|
||||
};
|
||||
|
||||
test('transform to correct link', () => {
|
||||
const post = path.join(blogPath, 'post.md');
|
||||
const post = path.join(contentPaths.contentPath, 'post.md');
|
||||
const [content, transformedContent] = transform(post);
|
||||
expect(transformedContent).toMatchSnapshot();
|
||||
expect(transformedContent).toContain(
|
||||
|
@ -54,3 +69,25 @@ test('transform to correct link', () => {
|
|||
);
|
||||
expect(content).not.toEqual(transformedContent);
|
||||
});
|
||||
|
||||
test('report broken markdown links', () => {
|
||||
const filePath = 'post-with-broken-links.md';
|
||||
const folderPath = contentPaths.contentPath;
|
||||
const postWithBrokenLinks = path.join(folderPath, filePath);
|
||||
const onBrokenMarkdownLink = jest.fn();
|
||||
const [, transformedContent] = transform(postWithBrokenLinks, {
|
||||
onBrokenMarkdownLink,
|
||||
});
|
||||
expect(transformedContent).toMatchSnapshot();
|
||||
expect(onBrokenMarkdownLink).toHaveBeenCalledTimes(2);
|
||||
expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(1, {
|
||||
filePath: path.resolve(folderPath, filePath),
|
||||
folderPath,
|
||||
link: 'postNotExist1.md',
|
||||
} as BlogBrokenMarkdownLink);
|
||||
expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(2, {
|
||||
filePath: path.resolve(folderPath, filePath),
|
||||
folderPath,
|
||||
link: './postNotExist2.mdx',
|
||||
} as BlogBrokenMarkdownLink);
|
||||
});
|
||||
|
|
|
@ -11,14 +11,23 @@ import chalk from 'chalk';
|
|||
import path from 'path';
|
||||
import readingTime from 'reading-time';
|
||||
import {Feed} from 'feed';
|
||||
import {PluginOptions, BlogPost, DateLink} from './types';
|
||||
import {
|
||||
PluginOptions,
|
||||
BlogPost,
|
||||
DateLink,
|
||||
BlogContentPaths,
|
||||
BlogBrokenMarkdownLink,
|
||||
BlogMarkdownLoaderOptions,
|
||||
} from './types';
|
||||
import {
|
||||
parseMarkdownFile,
|
||||
normalizeUrl,
|
||||
aliasedSitePath,
|
||||
getEditUrl,
|
||||
getFolderContainingFile,
|
||||
} from '@docusaurus/utils';
|
||||
import {LoadContext} from '@docusaurus/types';
|
||||
import {keyBy} from 'lodash';
|
||||
|
||||
export function truncate(fileString: string, truncateMarker: RegExp): string {
|
||||
return fileString.split(truncateMarker, 1).shift()!;
|
||||
|
@ -36,6 +45,7 @@ function toUrl({date, link}: DateLink) {
|
|||
}
|
||||
|
||||
export async function generateBlogFeed(
|
||||
contentPaths: BlogContentPaths,
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Promise<Feed | null> {
|
||||
|
@ -44,9 +54,8 @@ export async function generateBlogFeed(
|
|||
'Invalid options - `feedOptions` is not expected to be null.',
|
||||
);
|
||||
}
|
||||
const {siteDir, siteConfig} = context;
|
||||
const contentPath = path.resolve(siteDir, options.path);
|
||||
const blogPosts = await generateBlogPosts(contentPath, context, options);
|
||||
const {siteConfig} = context;
|
||||
const blogPosts = await generateBlogPosts(contentPaths, context, options);
|
||||
if (blogPosts == null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -88,7 +97,7 @@ export async function generateBlogFeed(
|
|||
}
|
||||
|
||||
export async function generateBlogPosts(
|
||||
blogDir: string,
|
||||
contentPaths: BlogContentPaths,
|
||||
{siteConfig, siteDir}: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Promise<BlogPost[]> {
|
||||
|
@ -100,24 +109,30 @@ export async function generateBlogPosts(
|
|||
editUrl,
|
||||
} = options;
|
||||
|
||||
if (!fs.existsSync(blogDir)) {
|
||||
if (!fs.existsSync(contentPaths.contentPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const {baseUrl = ''} = siteConfig;
|
||||
const blogFiles = await globby(include, {
|
||||
cwd: blogDir,
|
||||
const blogSourceFiles = await globby(include, {
|
||||
cwd: contentPaths.contentPath,
|
||||
});
|
||||
|
||||
const blogPosts: BlogPost[] = [];
|
||||
|
||||
await Promise.all(
|
||||
blogFiles.map(async (relativeSource: string) => {
|
||||
const source = path.join(blogDir, relativeSource);
|
||||
blogSourceFiles.map(async (blogSourceFile: string) => {
|
||||
// Lookup in localized folder in priority
|
||||
const contentPath = await getFolderContainingFile(
|
||||
getContentPathList(contentPaths),
|
||||
blogSourceFile,
|
||||
);
|
||||
|
||||
const source = path.join(contentPath, blogSourceFile);
|
||||
const aliasedSource = aliasedSitePath(source, siteDir);
|
||||
const refDir = path.parse(blogDir).dir;
|
||||
const relativePath = path.relative(refDir, source);
|
||||
const blogFileName = path.basename(relativeSource);
|
||||
|
||||
const relativePath = path.relative(siteDir, source);
|
||||
const blogFileName = path.basename(blogSourceFile);
|
||||
|
||||
const editBlogUrl = getEditUrl(relativePath, editUrl);
|
||||
|
||||
|
@ -184,12 +199,31 @@ export async function generateBlogPosts(
|
|||
return blogPosts;
|
||||
}
|
||||
|
||||
export function linkify(
|
||||
fileContent: string,
|
||||
siteDir: string,
|
||||
blogPath: string,
|
||||
blogPosts: BlogPost[],
|
||||
): string {
|
||||
export type LinkifyParams = {
|
||||
filePath: string;
|
||||
fileContent: string;
|
||||
} & Pick<
|
||||
BlogMarkdownLoaderOptions,
|
||||
'blogPosts' | 'siteDir' | 'contentPaths' | 'onBrokenMarkdownLink'
|
||||
>;
|
||||
|
||||
export function linkify({
|
||||
filePath,
|
||||
contentPaths,
|
||||
fileContent,
|
||||
siteDir,
|
||||
blogPosts,
|
||||
onBrokenMarkdownLink,
|
||||
}: LinkifyParams): string {
|
||||
// TODO temporary, should consider the file being in localized folder!
|
||||
const folderPath = contentPaths.contentPath;
|
||||
|
||||
// TODO perf refactor: do this earlier (once for all md files, not per file)
|
||||
const blogPostsBySource: Record<string, BlogPost> = keyBy(
|
||||
blogPosts,
|
||||
(item) => item.metadata.source,
|
||||
);
|
||||
|
||||
let fencedBlock = false;
|
||||
const lines = fileContent.split('\n').map((line) => {
|
||||
if (line.trim().startsWith('```')) {
|
||||
|
@ -208,18 +242,24 @@ export function linkify(
|
|||
const mdLink = mdMatch[1];
|
||||
const aliasedPostSource = `@site/${path.relative(
|
||||
siteDir,
|
||||
path.resolve(blogPath, mdLink),
|
||||
path.resolve(folderPath, mdLink),
|
||||
)}`;
|
||||
let blogPostPermalink = null;
|
||||
|
||||
blogPosts.forEach((blogPost) => {
|
||||
if (blogPost.metadata.source === aliasedPostSource) {
|
||||
blogPostPermalink = blogPost.metadata.permalink;
|
||||
}
|
||||
});
|
||||
const blogPost: BlogPost | undefined =
|
||||
blogPostsBySource[aliasedPostSource];
|
||||
|
||||
if (blogPostPermalink) {
|
||||
modifiedLine = modifiedLine.replace(mdLink, blogPostPermalink);
|
||||
if (blogPost) {
|
||||
modifiedLine = modifiedLine.replace(
|
||||
mdLink,
|
||||
blogPost.metadata.permalink,
|
||||
);
|
||||
} else {
|
||||
const brokenMarkdownLink: BlogBrokenMarkdownLink = {
|
||||
folderPath,
|
||||
filePath,
|
||||
link: mdLink,
|
||||
};
|
||||
onBrokenMarkdownLink(brokenMarkdownLink);
|
||||
}
|
||||
|
||||
mdMatch = mdRegex.exec(modifiedLine);
|
||||
|
@ -230,3 +270,8 @@ export function linkify(
|
|||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// Order matters: we look in priority in localized folder
|
||||
export function getContentPathList(contentPaths: BlogContentPaths) {
|
||||
return [contentPaths.contentPathLocalized, contentPaths.contentPath];
|
||||
}
|
||||
|
|
|
@ -8,13 +8,19 @@
|
|||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import admonitions from 'remark-admonitions';
|
||||
import {normalizeUrl, docuHash, aliasedSitePath} from '@docusaurus/utils';
|
||||
import {
|
||||
normalizeUrl,
|
||||
docuHash,
|
||||
aliasedSitePath,
|
||||
getPluginI18nPath,
|
||||
reportMessage,
|
||||
} from '@docusaurus/utils';
|
||||
import {
|
||||
STATIC_DIR_NAME,
|
||||
DEFAULT_PLUGIN_ID,
|
||||
} from '@docusaurus/core/lib/constants';
|
||||
import {ValidationError} from 'joi';
|
||||
import {take, kebabCase} from 'lodash';
|
||||
import {flatten, take, kebabCase} from 'lodash';
|
||||
|
||||
import {
|
||||
PluginOptions,
|
||||
|
@ -24,6 +30,8 @@ import {
|
|||
TagsModule,
|
||||
BlogPaginated,
|
||||
BlogPost,
|
||||
BlogContentPaths,
|
||||
BlogMarkdownLoaderOptions,
|
||||
} from './types';
|
||||
import {PluginOptionSchema} from './pluginOptionSchema';
|
||||
import {
|
||||
|
@ -36,7 +44,11 @@ import {
|
|||
ValidationResult,
|
||||
} from '@docusaurus/types';
|
||||
import {Configuration, Loader} from 'webpack';
|
||||
import {generateBlogFeed, generateBlogPosts} from './blogUtils';
|
||||
import {
|
||||
generateBlogFeed,
|
||||
generateBlogPosts,
|
||||
getContentPathList,
|
||||
} from './blogUtils';
|
||||
|
||||
export default function pluginContentBlog(
|
||||
context: LoadContext,
|
||||
|
@ -48,8 +60,22 @@ export default function pluginContentBlog(
|
|||
]);
|
||||
}
|
||||
|
||||
const {siteDir, generatedFilesDir} = context;
|
||||
const contentPath = path.resolve(siteDir, options.path);
|
||||
const {
|
||||
siteDir,
|
||||
siteConfig: {onBrokenMarkdownLinks},
|
||||
generatedFilesDir,
|
||||
i18n: {currentLocale},
|
||||
} = context;
|
||||
|
||||
const contentPaths: BlogContentPaths = {
|
||||
contentPath: path.resolve(siteDir, options.path),
|
||||
contentPathLocalized: getPluginI18nPath({
|
||||
siteDir,
|
||||
locale: currentLocale,
|
||||
pluginName: 'docusaurus-plugin-content-blog',
|
||||
pluginId: options.id,
|
||||
}),
|
||||
};
|
||||
const pluginId = options.id ?? DEFAULT_PLUGIN_ID;
|
||||
|
||||
const pluginDataDirRoot = path.join(
|
||||
|
@ -67,8 +93,11 @@ export default function pluginContentBlog(
|
|||
|
||||
getPathsToWatch() {
|
||||
const {include = []} = options;
|
||||
const globPattern = include.map((pattern) => `${contentPath}/${pattern}`);
|
||||
return [...globPattern];
|
||||
return flatten(
|
||||
getContentPathList(contentPaths).map((contentPath) => {
|
||||
return include.map((pattern) => `${contentPath}/${pattern}`);
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
getClientModules() {
|
||||
|
@ -85,7 +114,7 @@ export default function pluginContentBlog(
|
|||
async loadContent() {
|
||||
const {postsPerPage, routeBasePath} = options;
|
||||
|
||||
blogPosts = await generateBlogPosts(contentPath, context, options);
|
||||
blogPosts = await generateBlogPosts(contentPaths, context, options);
|
||||
if (!blogPosts.length) {
|
||||
return null;
|
||||
}
|
||||
|
@ -379,6 +408,23 @@ export default function pluginContentBlog(
|
|||
beforeDefaultRemarkPlugins,
|
||||
beforeDefaultRehypePlugins,
|
||||
} = options;
|
||||
|
||||
const markdownLoaderOptions: BlogMarkdownLoaderOptions = {
|
||||
siteDir,
|
||||
contentPaths,
|
||||
truncateMarker,
|
||||
blogPosts,
|
||||
onBrokenMarkdownLink: (brokenMarkdownLink) => {
|
||||
if (onBrokenMarkdownLinks === 'ignore') {
|
||||
return;
|
||||
}
|
||||
reportMessage(
|
||||
`Blog markdown link couldn't be resolved: (${brokenMarkdownLink.link}) in ${brokenMarkdownLink.filePath}`,
|
||||
onBrokenMarkdownLinks,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
resolve: {
|
||||
alias: {
|
||||
|
@ -389,7 +435,7 @@ export default function pluginContentBlog(
|
|||
rules: [
|
||||
{
|
||||
test: /(\.mdx?)$/,
|
||||
include: [contentPath],
|
||||
include: getContentPathList(contentPaths),
|
||||
use: [
|
||||
getCacheLoader(isServer),
|
||||
getBabelLoader(isServer),
|
||||
|
@ -414,12 +460,7 @@ export default function pluginContentBlog(
|
|||
},
|
||||
{
|
||||
loader: path.resolve(__dirname, './markdownLoader.js'),
|
||||
options: {
|
||||
siteDir,
|
||||
contentPath,
|
||||
truncateMarker,
|
||||
blogPosts,
|
||||
},
|
||||
options: markdownLoaderOptions,
|
||||
},
|
||||
].filter(Boolean) as Loader[],
|
||||
},
|
||||
|
@ -433,7 +474,7 @@ export default function pluginContentBlog(
|
|||
return;
|
||||
}
|
||||
|
||||
const feed = await generateBlogFeed(context, options);
|
||||
const feed = await generateBlogFeed(contentPaths, context, options);
|
||||
|
||||
if (!feed) {
|
||||
return;
|
||||
|
|
|
@ -8,19 +8,20 @@
|
|||
import {loader} from 'webpack';
|
||||
import {truncate, linkify} from './blogUtils';
|
||||
import {parseQuery, getOptions} from 'loader-utils';
|
||||
import {BlogMarkdownLoaderOptions} from './types';
|
||||
|
||||
const markdownLoader: loader.Loader = function (source) {
|
||||
const fileString = source as string;
|
||||
const filePath = this.resourcePath;
|
||||
const fileContent = source as string;
|
||||
const callback = this.async();
|
||||
const {truncateMarker, siteDir, contentPath, blogPosts} = getOptions(this);
|
||||
const markdownLoaderOptions = getOptions(this) as BlogMarkdownLoaderOptions;
|
||||
|
||||
// Linkify posts
|
||||
let finalContent = linkify(
|
||||
fileString as string,
|
||||
siteDir,
|
||||
contentPath,
|
||||
blogPosts,
|
||||
);
|
||||
// Linkify blog posts
|
||||
let finalContent = linkify({
|
||||
fileContent,
|
||||
filePath,
|
||||
...markdownLoaderOptions,
|
||||
});
|
||||
|
||||
// Truncate content if requested (e.g: file.md?truncated=true).
|
||||
const truncated: string | undefined = this.resourceQuery
|
||||
|
@ -28,7 +29,7 @@ const markdownLoader: loader.Loader = function (source) {
|
|||
: undefined;
|
||||
|
||||
if (truncated) {
|
||||
finalContent = truncate(finalContent, truncateMarker);
|
||||
finalContent = truncate(finalContent, markdownLoaderOptions.truncateMarker);
|
||||
}
|
||||
|
||||
return callback && callback(null, finalContent);
|
||||
|
|
|
@ -5,6 +5,11 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
export type BlogContentPaths = {
|
||||
contentPath: string;
|
||||
contentPathLocalized: string;
|
||||
};
|
||||
|
||||
export interface BlogContent {
|
||||
blogPosts: BlogPost[];
|
||||
blogListPaginated: BlogPaginated[];
|
||||
|
@ -121,3 +126,16 @@ export interface TagModule {
|
|||
count: number;
|
||||
permalink: string;
|
||||
}
|
||||
|
||||
export type BlogBrokenMarkdownLink = {
|
||||
folderPath: string;
|
||||
filePath: string;
|
||||
link: string;
|
||||
};
|
||||
export type BlogMarkdownLoaderOptions = {
|
||||
siteDir: string;
|
||||
contentPaths: BlogContentPaths;
|
||||
truncateMarker: RegExp;
|
||||
blogPosts: BlogPost[];
|
||||
onBrokenMarkdownLink: (brokenMarkdownLink: BlogBrokenMarkdownLink) => void;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Team title translated
|
||||
---
|
||||
|
||||
Team current version (translated)
|
|
@ -0,0 +1 @@
|
|||
Hello `1.0.0` ! (translated)
|
|
@ -616,18 +616,6 @@ Object {
|
|||
|
||||
exports[`versioned website (community) content: data 1`] = `
|
||||
Object {
|
||||
"site-community-team-md-9d8.json": "{
|
||||
\\"unversionedId\\": \\"team\\",
|
||||
\\"id\\": \\"team\\",
|
||||
\\"isDocsHomePage\\": false,
|
||||
\\"title\\": \\"team\\",
|
||||
\\"description\\": \\"Team current version\\",
|
||||
\\"source\\": \\"@site/community/team.md\\",
|
||||
\\"slug\\": \\"/team\\",
|
||||
\\"permalink\\": \\"/community/next/team\\",
|
||||
\\"version\\": \\"current\\",
|
||||
\\"sidebar\\": \\"community\\"
|
||||
}",
|
||||
"site-community-versioned-docs-version-1-0-0-team-md-359.json": "{
|
||||
\\"unversionedId\\": \\"team\\",
|
||||
\\"id\\": \\"version-1.0.0/team\\",
|
||||
|
@ -639,6 +627,18 @@ Object {
|
|||
\\"permalink\\": \\"/community/team\\",
|
||||
\\"version\\": \\"1.0.0\\",
|
||||
\\"sidebar\\": \\"version-1.0.0/community\\"
|
||||
}",
|
||||
"site-i-18-n-en-docusaurus-plugin-content-docs-community-current-team-md-7e5.json": "{
|
||||
\\"unversionedId\\": \\"team\\",
|
||||
\\"id\\": \\"team\\",
|
||||
\\"isDocsHomePage\\": false,
|
||||
\\"title\\": \\"Team title translated\\",
|
||||
\\"description\\": \\"Team current version (translated)\\",
|
||||
\\"source\\": \\"@site/i18n/en/docusaurus-plugin-content-docs-community/current/team.md\\",
|
||||
\\"slug\\": \\"/team\\",
|
||||
\\"permalink\\": \\"/community/next/team\\",
|
||||
\\"version\\": \\"current\\",
|
||||
\\"sidebar\\": \\"community\\"
|
||||
}",
|
||||
"version-1-0-0-metadata-prop-608.json": "{
|
||||
\\"pluginId\\": \\"community\\",
|
||||
|
@ -667,7 +667,7 @@ Object {
|
|||
\\"community\\": [
|
||||
{
|
||||
\\"type\\": \\"link\\",
|
||||
\\"label\\": \\"team\\",
|
||||
\\"label\\": \\"Team title translated\\",
|
||||
\\"href\\": \\"/community/next/team\\"
|
||||
}
|
||||
]
|
||||
|
@ -734,7 +734,7 @@ Array [
|
|||
"component": "@theme/DocItem",
|
||||
"exact": true,
|
||||
"modules": Object {
|
||||
"content": "@site/community/team.md",
|
||||
"content": "@site/i18n/en/docusaurus-plugin-content-docs-community/current/team.md",
|
||||
},
|
||||
"path": "/community/next/team",
|
||||
},
|
||||
|
@ -930,6 +930,22 @@ Object {
|
|||
\\"slug\\": \\"/tryToEscapeSlug\\",
|
||||
\\"permalink\\": \\"/docs/next/tryToEscapeSlug\\",
|
||||
\\"version\\": \\"current\\"
|
||||
}",
|
||||
"site-i-18-n-en-docusaurus-plugin-content-docs-version-1-0-0-hello-md-fe5.json": "{
|
||||
\\"unversionedId\\": \\"hello\\",
|
||||
\\"id\\": \\"version-1.0.0/hello\\",
|
||||
\\"isDocsHomePage\\": true,
|
||||
\\"title\\": \\"hello\\",
|
||||
\\"description\\": \\"Hello 1.0.0 ! (translated)\\",
|
||||
\\"source\\": \\"@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md\\",
|
||||
\\"slug\\": \\"/\\",
|
||||
\\"permalink\\": \\"/docs/1.0.0/\\",
|
||||
\\"version\\": \\"1.0.0\\",
|
||||
\\"sidebar\\": \\"version-1.0.0/docs\\",
|
||||
\\"previous\\": {
|
||||
\\"title\\": \\"baz\\",
|
||||
\\"permalink\\": \\"/docs/1.0.0/foo/baz\\"
|
||||
}
|
||||
}",
|
||||
"site-versioned-docs-version-1-0-0-foo-bar-md-7a6.json": "{
|
||||
\\"unversionedId\\": \\"foo/bar\\",
|
||||
|
@ -966,22 +982,6 @@ Object {
|
|||
\\"title\\": \\"hello\\",
|
||||
\\"permalink\\": \\"/docs/1.0.0/\\"
|
||||
}
|
||||
}",
|
||||
"site-versioned-docs-version-1-0-0-hello-md-3ef.json": "{
|
||||
\\"unversionedId\\": \\"hello\\",
|
||||
\\"id\\": \\"version-1.0.0/hello\\",
|
||||
\\"isDocsHomePage\\": true,
|
||||
\\"title\\": \\"hello\\",
|
||||
\\"description\\": \\"Hello 1.0.0 !\\",
|
||||
\\"source\\": \\"@site/versioned_docs/version-1.0.0/hello.md\\",
|
||||
\\"slug\\": \\"/\\",
|
||||
\\"permalink\\": \\"/docs/1.0.0/\\",
|
||||
\\"version\\": \\"1.0.0\\",
|
||||
\\"sidebar\\": \\"version-1.0.0/docs\\",
|
||||
\\"previous\\": {
|
||||
\\"title\\": \\"baz\\",
|
||||
\\"permalink\\": \\"/docs/1.0.0/foo/baz\\"
|
||||
}
|
||||
}",
|
||||
"site-versioned-docs-version-1-0-1-foo-bar-md-7a3.json": "{
|
||||
\\"unversionedId\\": \\"foo/bar\\",
|
||||
|
@ -1410,7 +1410,7 @@ Array [
|
|||
"component": "@theme/DocItem",
|
||||
"exact": true,
|
||||
"modules": Object {
|
||||
"content": "@site/versioned_docs/version-1.0.0/hello.md",
|
||||
"content": "@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md",
|
||||
},
|
||||
"path": "/docs/1.0.0/",
|
||||
},
|
||||
|
|
|
@ -0,0 +1,487 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`getLoadedContentTranslationFiles should return translation files matching snapshot 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"content": Object {
|
||||
"sidebar.docs.category.Getting started": Object {
|
||||
"description": "The label for category Getting started in sidebar docs",
|
||||
"message": "Getting started",
|
||||
},
|
||||
"sidebar.docs.link.Link label": Object {
|
||||
"description": "The label for link Link label in sidebar docs, linking to https://facebook.com",
|
||||
"message": "Link label",
|
||||
},
|
||||
"version.label": Object {
|
||||
"description": "The label for version current",
|
||||
"message": "current label",
|
||||
},
|
||||
},
|
||||
"path": "current",
|
||||
},
|
||||
Object {
|
||||
"content": Object {
|
||||
"sidebar.docs.category.Getting started": Object {
|
||||
"description": "The label for category Getting started in sidebar docs",
|
||||
"message": "Getting started",
|
||||
},
|
||||
"sidebar.docs.link.Link label": Object {
|
||||
"description": "The label for link Link label in sidebar docs, linking to https://facebook.com",
|
||||
"message": "Link label",
|
||||
},
|
||||
"version.label": Object {
|
||||
"description": "The label for version 2.0.0",
|
||||
"message": "2.0.0 label",
|
||||
},
|
||||
},
|
||||
"path": "version-2.0.0",
|
||||
},
|
||||
Object {
|
||||
"content": Object {
|
||||
"sidebar.docs.category.Getting started": Object {
|
||||
"description": "The label for category Getting started in sidebar docs",
|
||||
"message": "Getting started",
|
||||
},
|
||||
"sidebar.docs.link.Link label": Object {
|
||||
"description": "The label for link Link label in sidebar docs, linking to https://facebook.com",
|
||||
"message": "Link label",
|
||||
},
|
||||
"version.label": Object {
|
||||
"description": "The label for version 1.0.0",
|
||||
"message": "1.0.0 label",
|
||||
},
|
||||
},
|
||||
"path": "version-1.0.0",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`translateLoadedContent should return translated loaded content matching snapshot 1`] = `
|
||||
Object {
|
||||
"loadedVersions": Array [
|
||||
Object {
|
||||
"docs": Array [
|
||||
Object {
|
||||
"description": "doc1 description",
|
||||
"editUrl": "any",
|
||||
"id": "doc1",
|
||||
"isDocsHomePage": false,
|
||||
"lastUpdatedAt": 0,
|
||||
"lastUpdatedBy": "any",
|
||||
"next": undefined,
|
||||
"permalink": "any",
|
||||
"previous": undefined,
|
||||
"sidebar_label": "doc1 title",
|
||||
"slug": "any",
|
||||
"source": "any",
|
||||
"title": "doc1 title",
|
||||
"unversionedId": "any",
|
||||
"version": "any",
|
||||
},
|
||||
Object {
|
||||
"description": "doc2 description",
|
||||
"editUrl": "any",
|
||||
"id": "doc2",
|
||||
"isDocsHomePage": false,
|
||||
"lastUpdatedAt": 0,
|
||||
"lastUpdatedBy": "any",
|
||||
"next": undefined,
|
||||
"permalink": "any",
|
||||
"previous": undefined,
|
||||
"sidebar_label": "doc2 title",
|
||||
"slug": "any",
|
||||
"source": "any",
|
||||
"title": "doc2 title",
|
||||
"unversionedId": "any",
|
||||
"version": "any",
|
||||
},
|
||||
Object {
|
||||
"description": "doc3 description",
|
||||
"editUrl": "any",
|
||||
"id": "doc3",
|
||||
"isDocsHomePage": false,
|
||||
"lastUpdatedAt": 0,
|
||||
"lastUpdatedBy": "any",
|
||||
"next": undefined,
|
||||
"permalink": "any",
|
||||
"previous": undefined,
|
||||
"sidebar_label": "doc3 title",
|
||||
"slug": "any",
|
||||
"source": "any",
|
||||
"title": "doc3 title",
|
||||
"unversionedId": "any",
|
||||
"version": "any",
|
||||
},
|
||||
Object {
|
||||
"description": "doc4 description",
|
||||
"editUrl": "any",
|
||||
"id": "doc4",
|
||||
"isDocsHomePage": false,
|
||||
"lastUpdatedAt": 0,
|
||||
"lastUpdatedBy": "any",
|
||||
"next": undefined,
|
||||
"permalink": "any",
|
||||
"previous": undefined,
|
||||
"sidebar_label": "doc4 title",
|
||||
"slug": "any",
|
||||
"source": "any",
|
||||
"title": "doc4 title",
|
||||
"unversionedId": "any",
|
||||
"version": "any",
|
||||
},
|
||||
Object {
|
||||
"description": "doc5 description",
|
||||
"editUrl": "any",
|
||||
"id": "doc5",
|
||||
"isDocsHomePage": false,
|
||||
"lastUpdatedAt": 0,
|
||||
"lastUpdatedBy": "any",
|
||||
"next": undefined,
|
||||
"permalink": "any",
|
||||
"previous": undefined,
|
||||
"sidebar_label": "doc5 title",
|
||||
"slug": "any",
|
||||
"source": "any",
|
||||
"title": "doc5 title",
|
||||
"unversionedId": "any",
|
||||
"version": "any",
|
||||
},
|
||||
],
|
||||
"docsDirPath": "any",
|
||||
"docsDirPathLocalized": "any",
|
||||
"isLast": true,
|
||||
"mainDocId": "",
|
||||
"permalinkToSidebar": Object {},
|
||||
"routePriority": undefined,
|
||||
"sidebarFilePath": "any",
|
||||
"sidebars": Object {
|
||||
"docs": Array [
|
||||
Object {
|
||||
"collapsed": false,
|
||||
"items": Array [
|
||||
Object {
|
||||
"id": "doc1",
|
||||
"type": "doc",
|
||||
},
|
||||
Object {
|
||||
"id": "doc2",
|
||||
"type": "doc",
|
||||
},
|
||||
Object {
|
||||
"href": "https://facebook.com",
|
||||
"label": "Link label (translated)",
|
||||
"type": "link",
|
||||
},
|
||||
Object {
|
||||
"id": "doc1",
|
||||
"type": "ref",
|
||||
},
|
||||
],
|
||||
"label": "Getting started (translated)",
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
"id": "doc3",
|
||||
"type": "doc",
|
||||
},
|
||||
],
|
||||
"otherSidebar": Array [
|
||||
Object {
|
||||
"id": "doc4",
|
||||
"type": "doc",
|
||||
},
|
||||
Object {
|
||||
"id": "doc5",
|
||||
"type": "doc",
|
||||
},
|
||||
],
|
||||
},
|
||||
"versionLabel": "current label (translated)",
|
||||
"versionName": "current",
|
||||
"versionPath": "/docs/",
|
||||
},
|
||||
Object {
|
||||
"docs": Array [
|
||||
Object {
|
||||
"description": "doc1 description",
|
||||
"editUrl": "any",
|
||||
"id": "doc1",
|
||||
"isDocsHomePage": false,
|
||||
"lastUpdatedAt": 0,
|
||||
"lastUpdatedBy": "any",
|
||||
"next": undefined,
|
||||
"permalink": "any",
|
||||
"previous": undefined,
|
||||
"sidebar_label": "doc1 title",
|
||||
"slug": "any",
|
||||
"source": "any",
|
||||
"title": "doc1 title",
|
||||
"unversionedId": "any",
|
||||
"version": "any",
|
||||
},
|
||||
Object {
|
||||
"description": "doc2 description",
|
||||
"editUrl": "any",
|
||||
"id": "doc2",
|
||||
"isDocsHomePage": false,
|
||||
"lastUpdatedAt": 0,
|
||||
"lastUpdatedBy": "any",
|
||||
"next": undefined,
|
||||
"permalink": "any",
|
||||
"previous": undefined,
|
||||
"sidebar_label": "doc2 title",
|
||||
"slug": "any",
|
||||
"source": "any",
|
||||
"title": "doc2 title",
|
||||
"unversionedId": "any",
|
||||
"version": "any",
|
||||
},
|
||||
Object {
|
||||
"description": "doc3 description",
|
||||
"editUrl": "any",
|
||||
"id": "doc3",
|
||||
"isDocsHomePage": false,
|
||||
"lastUpdatedAt": 0,
|
||||
"lastUpdatedBy": "any",
|
||||
"next": undefined,
|
||||
"permalink": "any",
|
||||
"previous": undefined,
|
||||
"sidebar_label": "doc3 title",
|
||||
"slug": "any",
|
||||
"source": "any",
|
||||
"title": "doc3 title",
|
||||
"unversionedId": "any",
|
||||
"version": "any",
|
||||
},
|
||||
Object {
|
||||
"description": "doc4 description",
|
||||
"editUrl": "any",
|
||||
"id": "doc4",
|
||||
"isDocsHomePage": false,
|
||||
"lastUpdatedAt": 0,
|
||||
"lastUpdatedBy": "any",
|
||||
"next": undefined,
|
||||
"permalink": "any",
|
||||
"previous": undefined,
|
||||
"sidebar_label": "doc4 title",
|
||||
"slug": "any",
|
||||
"source": "any",
|
||||
"title": "doc4 title",
|
||||
"unversionedId": "any",
|
||||
"version": "any",
|
||||
},
|
||||
Object {
|
||||
"description": "doc5 description",
|
||||
"editUrl": "any",
|
||||
"id": "doc5",
|
||||
"isDocsHomePage": false,
|
||||
"lastUpdatedAt": 0,
|
||||
"lastUpdatedBy": "any",
|
||||
"next": undefined,
|
||||
"permalink": "any",
|
||||
"previous": undefined,
|
||||
"sidebar_label": "doc5 title",
|
||||
"slug": "any",
|
||||
"source": "any",
|
||||
"title": "doc5 title",
|
||||
"unversionedId": "any",
|
||||
"version": "any",
|
||||
},
|
||||
],
|
||||
"docsDirPath": "any",
|
||||
"docsDirPathLocalized": "any",
|
||||
"isLast": true,
|
||||
"mainDocId": "",
|
||||
"permalinkToSidebar": Object {},
|
||||
"routePriority": undefined,
|
||||
"sidebarFilePath": "any",
|
||||
"sidebars": Object {
|
||||
"docs": Array [
|
||||
Object {
|
||||
"collapsed": false,
|
||||
"items": Array [
|
||||
Object {
|
||||
"id": "doc1",
|
||||
"type": "doc",
|
||||
},
|
||||
Object {
|
||||
"id": "doc2",
|
||||
"type": "doc",
|
||||
},
|
||||
Object {
|
||||
"href": "https://facebook.com",
|
||||
"label": "Link label (translated)",
|
||||
"type": "link",
|
||||
},
|
||||
Object {
|
||||
"id": "doc1",
|
||||
"type": "ref",
|
||||
},
|
||||
],
|
||||
"label": "Getting started (translated)",
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
"id": "doc3",
|
||||
"type": "doc",
|
||||
},
|
||||
],
|
||||
"otherSidebar": Array [
|
||||
Object {
|
||||
"id": "doc4",
|
||||
"type": "doc",
|
||||
},
|
||||
Object {
|
||||
"id": "doc5",
|
||||
"type": "doc",
|
||||
},
|
||||
],
|
||||
},
|
||||
"versionLabel": "2.0.0 label (translated)",
|
||||
"versionName": "2.0.0",
|
||||
"versionPath": "/docs/",
|
||||
},
|
||||
Object {
|
||||
"docs": Array [
|
||||
Object {
|
||||
"description": "doc1 description",
|
||||
"editUrl": "any",
|
||||
"id": "doc1",
|
||||
"isDocsHomePage": false,
|
||||
"lastUpdatedAt": 0,
|
||||
"lastUpdatedBy": "any",
|
||||
"next": undefined,
|
||||
"permalink": "any",
|
||||
"previous": undefined,
|
||||
"sidebar_label": "doc1 title",
|
||||
"slug": "any",
|
||||
"source": "any",
|
||||
"title": "doc1 title",
|
||||
"unversionedId": "any",
|
||||
"version": "any",
|
||||
},
|
||||
Object {
|
||||
"description": "doc2 description",
|
||||
"editUrl": "any",
|
||||
"id": "doc2",
|
||||
"isDocsHomePage": false,
|
||||
"lastUpdatedAt": 0,
|
||||
"lastUpdatedBy": "any",
|
||||
"next": undefined,
|
||||
"permalink": "any",
|
||||
"previous": undefined,
|
||||
"sidebar_label": "doc2 title",
|
||||
"slug": "any",
|
||||
"source": "any",
|
||||
"title": "doc2 title",
|
||||
"unversionedId": "any",
|
||||
"version": "any",
|
||||
},
|
||||
Object {
|
||||
"description": "doc3 description",
|
||||
"editUrl": "any",
|
||||
"id": "doc3",
|
||||
"isDocsHomePage": false,
|
||||
"lastUpdatedAt": 0,
|
||||
"lastUpdatedBy": "any",
|
||||
"next": undefined,
|
||||
"permalink": "any",
|
||||
"previous": undefined,
|
||||
"sidebar_label": "doc3 title",
|
||||
"slug": "any",
|
||||
"source": "any",
|
||||
"title": "doc3 title",
|
||||
"unversionedId": "any",
|
||||
"version": "any",
|
||||
},
|
||||
Object {
|
||||
"description": "doc4 description",
|
||||
"editUrl": "any",
|
||||
"id": "doc4",
|
||||
"isDocsHomePage": false,
|
||||
"lastUpdatedAt": 0,
|
||||
"lastUpdatedBy": "any",
|
||||
"next": undefined,
|
||||
"permalink": "any",
|
||||
"previous": undefined,
|
||||
"sidebar_label": "doc4 title",
|
||||
"slug": "any",
|
||||
"source": "any",
|
||||
"title": "doc4 title",
|
||||
"unversionedId": "any",
|
||||
"version": "any",
|
||||
},
|
||||
Object {
|
||||
"description": "doc5 description",
|
||||
"editUrl": "any",
|
||||
"id": "doc5",
|
||||
"isDocsHomePage": false,
|
||||
"lastUpdatedAt": 0,
|
||||
"lastUpdatedBy": "any",
|
||||
"next": undefined,
|
||||
"permalink": "any",
|
||||
"previous": undefined,
|
||||
"sidebar_label": "doc5 title",
|
||||
"slug": "any",
|
||||
"source": "any",
|
||||
"title": "doc5 title",
|
||||
"unversionedId": "any",
|
||||
"version": "any",
|
||||
},
|
||||
],
|
||||
"docsDirPath": "any",
|
||||
"docsDirPathLocalized": "any",
|
||||
"isLast": true,
|
||||
"mainDocId": "",
|
||||
"permalinkToSidebar": Object {},
|
||||
"routePriority": undefined,
|
||||
"sidebarFilePath": "any",
|
||||
"sidebars": Object {
|
||||
"docs": Array [
|
||||
Object {
|
||||
"collapsed": false,
|
||||
"items": Array [
|
||||
Object {
|
||||
"id": "doc1",
|
||||
"type": "doc",
|
||||
},
|
||||
Object {
|
||||
"id": "doc2",
|
||||
"type": "doc",
|
||||
},
|
||||
Object {
|
||||
"href": "https://facebook.com",
|
||||
"label": "Link label (translated)",
|
||||
"type": "link",
|
||||
},
|
||||
Object {
|
||||
"id": "doc1",
|
||||
"type": "ref",
|
||||
},
|
||||
],
|
||||
"label": "Getting started (translated)",
|
||||
"type": "category",
|
||||
},
|
||||
Object {
|
||||
"id": "doc3",
|
||||
"type": "doc",
|
||||
},
|
||||
],
|
||||
"otherSidebar": Array [
|
||||
Object {
|
||||
"id": "doc4",
|
||||
"type": "doc",
|
||||
},
|
||||
Object {
|
||||
"id": "doc5",
|
||||
"type": "doc",
|
||||
},
|
||||
],
|
||||
},
|
||||
"versionLabel": "1.0.0 label (translated)",
|
||||
"versionName": "1.0.0",
|
||||
"versionPath": "/docs/",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
|
@ -42,6 +42,7 @@ ${markdown}
|
|||
source,
|
||||
content,
|
||||
lastUpdate: {},
|
||||
filePath: source,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -57,7 +58,7 @@ function createTestUtils({
|
|||
options: MetadataOptions;
|
||||
}) {
|
||||
async function readDoc(docFileSource: string) {
|
||||
return readDocFile(versionMetadata.docsDirPath, docFileSource, options);
|
||||
return readDocFile(versionMetadata, docFileSource, options);
|
||||
}
|
||||
function processDocFile(docFile: DocFile) {
|
||||
return processDocMetadata({
|
||||
|
@ -110,30 +111,41 @@ function createTestUtils({
|
|||
}
|
||||
|
||||
describe('simple site', () => {
|
||||
const siteDir = path.join(fixtureDir, 'simple-site');
|
||||
const context = loadContext(siteDir);
|
||||
const options = {
|
||||
id: DEFAULT_PLUGIN_ID,
|
||||
...DEFAULT_OPTIONS,
|
||||
};
|
||||
const versionsMetadata = readVersionsMetadata({
|
||||
context,
|
||||
options: {
|
||||
async function loadSite() {
|
||||
const siteDir = path.join(fixtureDir, 'simple-site');
|
||||
const context = await loadContext(siteDir);
|
||||
const options = {
|
||||
id: DEFAULT_PLUGIN_ID,
|
||||
...DEFAULT_OPTIONS,
|
||||
},
|
||||
});
|
||||
expect(versionsMetadata.length).toEqual(1);
|
||||
const [currentVersion] = versionsMetadata;
|
||||
};
|
||||
const versionsMetadata = readVersionsMetadata({
|
||||
context,
|
||||
options: {
|
||||
id: DEFAULT_PLUGIN_ID,
|
||||
...DEFAULT_OPTIONS,
|
||||
},
|
||||
});
|
||||
expect(versionsMetadata.length).toEqual(1);
|
||||
const [currentVersion] = versionsMetadata;
|
||||
|
||||
const defaultTestUtils = createTestUtils({
|
||||
siteDir,
|
||||
context,
|
||||
options,
|
||||
versionMetadata: currentVersion,
|
||||
});
|
||||
const defaultTestUtils = createTestUtils({
|
||||
siteDir,
|
||||
context,
|
||||
options,
|
||||
versionMetadata: currentVersion,
|
||||
});
|
||||
return {
|
||||
siteDir,
|
||||
context,
|
||||
options,
|
||||
versionsMetadata,
|
||||
defaultTestUtils,
|
||||
currentVersion,
|
||||
};
|
||||
}
|
||||
|
||||
test('readVersionDocs', async () => {
|
||||
const {options, currentVersion} = await loadSite();
|
||||
const docs = await readVersionDocs(currentVersion, options);
|
||||
expect(docs.map((doc) => doc.source).sort()).toEqual(
|
||||
[
|
||||
|
@ -155,6 +167,7 @@ describe('simple site', () => {
|
|||
});
|
||||
|
||||
test('normal docs', async () => {
|
||||
const {defaultTestUtils} = await loadSite();
|
||||
await defaultTestUtils.testMeta(path.join('foo', 'bar.md'), {
|
||||
version: 'current',
|
||||
id: 'foo/bar',
|
||||
|
@ -178,6 +191,8 @@ describe('simple site', () => {
|
|||
});
|
||||
|
||||
test('homePageId doc', async () => {
|
||||
const {siteDir, context, options, currentVersion} = await loadSite();
|
||||
|
||||
const testUtilsLocal = createTestUtils({
|
||||
siteDir,
|
||||
context,
|
||||
|
@ -198,6 +213,8 @@ describe('simple site', () => {
|
|||
});
|
||||
|
||||
test('homePageId doc nested', async () => {
|
||||
const {siteDir, context, options, currentVersion} = await loadSite();
|
||||
|
||||
const testUtilsLocal = createTestUtils({
|
||||
siteDir,
|
||||
context,
|
||||
|
@ -218,6 +235,8 @@ describe('simple site', () => {
|
|||
});
|
||||
|
||||
test('docs with editUrl', async () => {
|
||||
const {siteDir, context, options, currentVersion} = await loadSite();
|
||||
|
||||
const testUtilsLocal = createTestUtils({
|
||||
siteDir,
|
||||
context,
|
||||
|
@ -243,6 +262,8 @@ describe('simple site', () => {
|
|||
});
|
||||
|
||||
test('docs with custom editUrl & unrelated frontmatter', async () => {
|
||||
const {defaultTestUtils} = await loadSite();
|
||||
|
||||
await defaultTestUtils.testMeta('lorem.md', {
|
||||
version: 'current',
|
||||
id: 'lorem',
|
||||
|
@ -257,6 +278,8 @@ describe('simple site', () => {
|
|||
});
|
||||
|
||||
test('docs with last update time and author', async () => {
|
||||
const {siteDir, context, options, currentVersion} = await loadSite();
|
||||
|
||||
const testUtilsLocal = createTestUtils({
|
||||
siteDir,
|
||||
context,
|
||||
|
@ -284,6 +307,8 @@ describe('simple site', () => {
|
|||
});
|
||||
|
||||
test('docs with slugs', async () => {
|
||||
const {defaultTestUtils} = await loadSite();
|
||||
|
||||
await defaultTestUtils.testSlug(
|
||||
path.join('rootRelativeSlug.md'),
|
||||
'/docs/rootRelativeSlug',
|
||||
|
@ -319,7 +344,8 @@ describe('simple site', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('docs with invalid id', () => {
|
||||
test('docs with invalid id', async () => {
|
||||
const {defaultTestUtils} = await loadSite();
|
||||
expect(() => {
|
||||
defaultTestUtils.processDocFile(
|
||||
createFakeDocFile({
|
||||
|
@ -335,6 +361,8 @@ describe('simple site', () => {
|
|||
});
|
||||
|
||||
test('docs with slug on doc home', async () => {
|
||||
const {siteDir, context, options, currentVersion} = await loadSite();
|
||||
|
||||
const testUtilsLocal = createTestUtils({
|
||||
siteDir,
|
||||
context,
|
||||
|
@ -360,55 +388,71 @@ describe('simple site', () => {
|
|||
});
|
||||
|
||||
describe('versioned site', () => {
|
||||
const siteDir = path.join(fixtureDir, 'versioned-site');
|
||||
const context = loadContext(siteDir);
|
||||
const options = {
|
||||
id: DEFAULT_PLUGIN_ID,
|
||||
...DEFAULT_OPTIONS,
|
||||
};
|
||||
const versionsMetadata = readVersionsMetadata({
|
||||
context,
|
||||
options: {
|
||||
async function loadSite() {
|
||||
const siteDir = path.join(fixtureDir, 'versioned-site');
|
||||
const context = await loadContext(siteDir);
|
||||
const options = {
|
||||
id: DEFAULT_PLUGIN_ID,
|
||||
...DEFAULT_OPTIONS,
|
||||
},
|
||||
});
|
||||
expect(versionsMetadata.length).toEqual(4);
|
||||
const [
|
||||
currentVersion,
|
||||
version101,
|
||||
version100,
|
||||
versionWithSlugs,
|
||||
] = versionsMetadata;
|
||||
};
|
||||
const versionsMetadata = readVersionsMetadata({
|
||||
context,
|
||||
options: {
|
||||
id: DEFAULT_PLUGIN_ID,
|
||||
...DEFAULT_OPTIONS,
|
||||
},
|
||||
});
|
||||
expect(versionsMetadata.length).toEqual(4);
|
||||
const [
|
||||
currentVersion,
|
||||
version101,
|
||||
version100,
|
||||
versionWithSlugs,
|
||||
] = versionsMetadata;
|
||||
|
||||
const currentVersionTestUtils = createTestUtils({
|
||||
siteDir,
|
||||
context,
|
||||
options,
|
||||
versionMetadata: currentVersion,
|
||||
});
|
||||
const version101TestUtils = createTestUtils({
|
||||
siteDir,
|
||||
context,
|
||||
options,
|
||||
versionMetadata: version101,
|
||||
});
|
||||
const currentVersionTestUtils = createTestUtils({
|
||||
siteDir,
|
||||
context,
|
||||
options,
|
||||
versionMetadata: currentVersion,
|
||||
});
|
||||
const version101TestUtils = createTestUtils({
|
||||
siteDir,
|
||||
context,
|
||||
options,
|
||||
versionMetadata: version101,
|
||||
});
|
||||
|
||||
const version100TestUtils = createTestUtils({
|
||||
siteDir,
|
||||
context,
|
||||
options,
|
||||
versionMetadata: version100,
|
||||
});
|
||||
const version100TestUtils = createTestUtils({
|
||||
siteDir,
|
||||
context,
|
||||
options,
|
||||
versionMetadata: version100,
|
||||
});
|
||||
|
||||
const versionWithSlugsTestUtils = createTestUtils({
|
||||
siteDir,
|
||||
context,
|
||||
options,
|
||||
versionMetadata: versionWithSlugs,
|
||||
});
|
||||
const versionWithSlugsTestUtils = createTestUtils({
|
||||
siteDir,
|
||||
context,
|
||||
options,
|
||||
versionMetadata: versionWithSlugs,
|
||||
});
|
||||
|
||||
return {
|
||||
siteDir,
|
||||
context,
|
||||
options,
|
||||
versionsMetadata,
|
||||
currentVersionTestUtils,
|
||||
version101TestUtils,
|
||||
version100,
|
||||
version100TestUtils,
|
||||
versionWithSlugsTestUtils,
|
||||
};
|
||||
}
|
||||
|
||||
test('next docs', async () => {
|
||||
const {currentVersionTestUtils} = await loadSite();
|
||||
|
||||
await currentVersionTestUtils.testMeta(path.join('foo', 'bar.md'), {
|
||||
id: 'foo/bar',
|
||||
unversionedId: 'foo/bar',
|
||||
|
@ -432,6 +476,8 @@ describe('versioned site', () => {
|
|||
});
|
||||
|
||||
test('versioned docs', async () => {
|
||||
const {version101TestUtils, version100TestUtils} = await loadSite();
|
||||
|
||||
await version100TestUtils.testMeta(path.join('foo', 'bar.md'), {
|
||||
id: 'version-1.0.0/foo/bar',
|
||||
unversionedId: 'foo/bar',
|
||||
|
@ -449,8 +495,10 @@ describe('versioned site', () => {
|
|||
permalink: '/docs/1.0.0/hello',
|
||||
slug: '/hello',
|
||||
title: 'hello',
|
||||
description: 'Hello 1.0.0 !',
|
||||
description: 'Hello 1.0.0 ! (translated)',
|
||||
version: '1.0.0',
|
||||
source:
|
||||
'@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md',
|
||||
});
|
||||
await version101TestUtils.testMeta(path.join('foo', 'bar.md'), {
|
||||
id: 'version-1.0.1/foo/bar',
|
||||
|
@ -475,6 +523,8 @@ describe('versioned site', () => {
|
|||
});
|
||||
|
||||
test('next doc slugs', async () => {
|
||||
const {currentVersionTestUtils} = await loadSite();
|
||||
|
||||
await currentVersionTestUtils.testSlug(
|
||||
path.join('slugs', 'absoluteSlug.md'),
|
||||
'/docs/next/absoluteSlug',
|
||||
|
@ -494,6 +544,8 @@ describe('versioned site', () => {
|
|||
});
|
||||
|
||||
test('versioned doc slugs', async () => {
|
||||
const {versionWithSlugsTestUtils} = await loadSite();
|
||||
|
||||
await versionWithSlugsTestUtils.testSlug(
|
||||
path.join('rootAbsoluteSlug.md'),
|
||||
'/docs/withSlugs/rootAbsoluteSlug',
|
||||
|
@ -528,4 +580,33 @@ describe('versioned site', () => {
|
|||
'/docs/withSlugs/tryToEscapeSlug',
|
||||
);
|
||||
});
|
||||
|
||||
test('translated doc with editUrl', async () => {
|
||||
const {siteDir, context, options, version100} = await loadSite();
|
||||
|
||||
const testUtilsLocal = createTestUtils({
|
||||
siteDir,
|
||||
context,
|
||||
options: {
|
||||
...options,
|
||||
editUrl: 'https://github.com/facebook/docusaurus/edit/master/website',
|
||||
},
|
||||
versionMetadata: version100,
|
||||
});
|
||||
|
||||
await testUtilsLocal.testMeta(path.join('hello.md'), {
|
||||
id: 'version-1.0.0/hello',
|
||||
unversionedId: 'hello',
|
||||
isDocsHomePage: false,
|
||||
permalink: '/docs/1.0.0/hello',
|
||||
slug: '/hello',
|
||||
title: 'hello',
|
||||
description: 'Hello 1.0.0 ! (translated)',
|
||||
version: '1.0.0',
|
||||
source:
|
||||
'@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md',
|
||||
editUrl:
|
||||
'https://github.com/facebook/docusaurus/edit/master/website/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -106,7 +106,7 @@ Entries created:
|
|||
|
||||
test('site with wrong sidebar file', async () => {
|
||||
const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
|
||||
const context = loadContext(siteDir);
|
||||
const context = await loadContext(siteDir);
|
||||
const sidebarPath = path.join(siteDir, 'wrong-sidebars.json');
|
||||
const plugin = pluginContentDocs(
|
||||
context,
|
||||
|
@ -119,9 +119,9 @@ test('site with wrong sidebar file', async () => {
|
|||
|
||||
describe('empty/no docs website', () => {
|
||||
const siteDir = path.join(__dirname, '__fixtures__', 'empty-site');
|
||||
const context = loadContext(siteDir);
|
||||
|
||||
test('no files in docs folder', async () => {
|
||||
const context = await loadContext(siteDir);
|
||||
await fs.ensureDir(path.join(siteDir, 'docs'));
|
||||
const plugin = pluginContentDocs(
|
||||
context,
|
||||
|
@ -135,6 +135,7 @@ describe('empty/no docs website', () => {
|
|||
});
|
||||
|
||||
test('docs folder does not exist', async () => {
|
||||
const context = await loadContext(siteDir);
|
||||
expect(() =>
|
||||
pluginContentDocs(
|
||||
context,
|
||||
|
@ -149,20 +150,25 @@ describe('empty/no docs website', () => {
|
|||
});
|
||||
|
||||
describe('simple website', () => {
|
||||
const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
|
||||
const context = loadContext(siteDir);
|
||||
const sidebarPath = path.join(siteDir, 'sidebars.json');
|
||||
const plugin = pluginContentDocs(
|
||||
context,
|
||||
normalizePluginOptions(OptionsSchema, {
|
||||
path: 'docs',
|
||||
sidebarPath,
|
||||
homePageId: 'hello',
|
||||
}),
|
||||
);
|
||||
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
|
||||
async function loadSite() {
|
||||
const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
|
||||
const context = await loadContext(siteDir);
|
||||
const sidebarPath = path.join(siteDir, 'sidebars.json');
|
||||
const plugin = pluginContentDocs(
|
||||
context,
|
||||
normalizePluginOptions(OptionsSchema, {
|
||||
path: 'docs',
|
||||
sidebarPath,
|
||||
homePageId: 'hello',
|
||||
}),
|
||||
);
|
||||
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
|
||||
|
||||
test('extendCli - docsVersion', () => {
|
||||
return {siteDir, context, sidebarPath, plugin, pluginContentDir};
|
||||
}
|
||||
|
||||
test('extendCli - docsVersion', async () => {
|
||||
const {siteDir, sidebarPath, plugin} = await loadSite();
|
||||
const mock = jest
|
||||
.spyOn(cliDocs, 'cliDocsVersionCommand')
|
||||
.mockImplementation();
|
||||
|
@ -178,7 +184,9 @@ describe('simple website', () => {
|
|||
mock.mockRestore();
|
||||
});
|
||||
|
||||
test('getPathToWatch', () => {
|
||||
test('getPathToWatch', async () => {
|
||||
const {siteDir, plugin} = await loadSite();
|
||||
|
||||
const pathToWatch = plugin.getPathsToWatch!();
|
||||
const matchPattern = pathToWatch.map((filepath) =>
|
||||
posixPath(path.relative(siteDir, filepath)),
|
||||
|
@ -187,6 +195,7 @@ describe('simple website', () => {
|
|||
expect(matchPattern).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"sidebars.json",
|
||||
"i18n/en/docusaurus-plugin-content-docs/current/**/*.{md,mdx}",
|
||||
"docs/**/*.{md,mdx}",
|
||||
]
|
||||
`);
|
||||
|
@ -203,6 +212,8 @@ describe('simple website', () => {
|
|||
});
|
||||
|
||||
test('configureWebpack', async () => {
|
||||
const {plugin} = await loadSite();
|
||||
|
||||
const config = applyConfigureWebpack(
|
||||
plugin.configureWebpack,
|
||||
{
|
||||
|
@ -219,6 +230,7 @@ describe('simple website', () => {
|
|||
});
|
||||
|
||||
test('content', async () => {
|
||||
const {siteDir, plugin, pluginContentDir} = await loadSite();
|
||||
const content = await plugin.loadContent!();
|
||||
expect(content.loadedVersions.length).toEqual(1);
|
||||
const [currentVersion] = content.loadedVersions;
|
||||
|
@ -287,22 +299,32 @@ describe('simple website', () => {
|
|||
});
|
||||
|
||||
describe('versioned website', () => {
|
||||
const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site');
|
||||
const context = loadContext(siteDir);
|
||||
const sidebarPath = path.join(siteDir, 'sidebars.json');
|
||||
const routeBasePath = 'docs';
|
||||
const plugin = pluginContentDocs(
|
||||
context,
|
||||
normalizePluginOptions(OptionsSchema, {
|
||||
async function loadSite() {
|
||||
const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site');
|
||||
const context = await loadContext(siteDir);
|
||||
const sidebarPath = path.join(siteDir, 'sidebars.json');
|
||||
const routeBasePath = 'docs';
|
||||
const plugin = pluginContentDocs(
|
||||
context,
|
||||
normalizePluginOptions(OptionsSchema, {
|
||||
routeBasePath,
|
||||
sidebarPath,
|
||||
homePageId: 'hello',
|
||||
}),
|
||||
);
|
||||
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
|
||||
return {
|
||||
siteDir,
|
||||
context,
|
||||
routeBasePath,
|
||||
sidebarPath,
|
||||
homePageId: 'hello',
|
||||
}),
|
||||
);
|
||||
plugin,
|
||||
pluginContentDir,
|
||||
};
|
||||
}
|
||||
|
||||
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
|
||||
|
||||
test('extendCli - docsVersion', () => {
|
||||
test('extendCli - docsVersion', async () => {
|
||||
const {siteDir, routeBasePath, sidebarPath, plugin} = await loadSite();
|
||||
const mock = jest
|
||||
.spyOn(cliDocs, 'cliDocsVersionCommand')
|
||||
.mockImplementation();
|
||||
|
@ -318,7 +340,8 @@ describe('versioned website', () => {
|
|||
mock.mockRestore();
|
||||
});
|
||||
|
||||
test('getPathToWatch', () => {
|
||||
test('getPathToWatch', async () => {
|
||||
const {siteDir, plugin} = await loadSite();
|
||||
const pathToWatch = plugin.getPathsToWatch!();
|
||||
const matchPattern = pathToWatch.map((filepath) =>
|
||||
posixPath(path.relative(siteDir, filepath)),
|
||||
|
@ -327,12 +350,16 @@ describe('versioned website', () => {
|
|||
expect(matchPattern).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"sidebars.json",
|
||||
"i18n/en/docusaurus-plugin-content-docs/current/**/*.{md,mdx}",
|
||||
"docs/**/*.{md,mdx}",
|
||||
"versioned_sidebars/version-1.0.1-sidebars.json",
|
||||
"i18n/en/docusaurus-plugin-content-docs/version-1.0.1/**/*.{md,mdx}",
|
||||
"versioned_docs/version-1.0.1/**/*.{md,mdx}",
|
||||
"versioned_sidebars/version-1.0.0-sidebars.json",
|
||||
"i18n/en/docusaurus-plugin-content-docs/version-1.0.0/**/*.{md,mdx}",
|
||||
"versioned_docs/version-1.0.0/**/*.{md,mdx}",
|
||||
"versioned_sidebars/version-withSlugs-sidebars.json",
|
||||
"i18n/en/docusaurus-plugin-content-docs/version-withSlugs/**/*.{md,mdx}",
|
||||
"versioned_docs/version-withSlugs/**/*.{md,mdx}",
|
||||
]
|
||||
`);
|
||||
|
@ -369,6 +396,7 @@ describe('versioned website', () => {
|
|||
});
|
||||
|
||||
test('content', async () => {
|
||||
const {siteDir, plugin, pluginContentDir} = await loadSite();
|
||||
const content = await plugin.loadContent!();
|
||||
expect(content.loadedVersions.length).toEqual(4);
|
||||
const [
|
||||
|
@ -499,23 +527,41 @@ describe('versioned website', () => {
|
|||
});
|
||||
|
||||
describe('versioned website (community)', () => {
|
||||
const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site');
|
||||
const context = loadContext(siteDir);
|
||||
const sidebarPath = path.join(siteDir, 'community_sidebars.json');
|
||||
const routeBasePath = 'community';
|
||||
const pluginId = 'community';
|
||||
const plugin = pluginContentDocs(
|
||||
context,
|
||||
normalizePluginOptions(OptionsSchema, {
|
||||
id: 'community',
|
||||
path: 'community',
|
||||
async function loadSite() {
|
||||
const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site');
|
||||
const context = await loadContext(siteDir);
|
||||
const sidebarPath = path.join(siteDir, 'community_sidebars.json');
|
||||
const routeBasePath = 'community';
|
||||
const pluginId = 'community';
|
||||
const plugin = pluginContentDocs(
|
||||
context,
|
||||
normalizePluginOptions(OptionsSchema, {
|
||||
id: 'community',
|
||||
path: 'community',
|
||||
routeBasePath,
|
||||
sidebarPath,
|
||||
}),
|
||||
);
|
||||
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
|
||||
return {
|
||||
siteDir,
|
||||
context,
|
||||
routeBasePath,
|
||||
sidebarPath,
|
||||
}),
|
||||
);
|
||||
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
|
||||
pluginId,
|
||||
plugin,
|
||||
pluginContentDir,
|
||||
};
|
||||
}
|
||||
|
||||
test('extendCli - docsVersion', () => {
|
||||
test('extendCli - docsVersion', async () => {
|
||||
const {
|
||||
siteDir,
|
||||
routeBasePath,
|
||||
sidebarPath,
|
||||
pluginId,
|
||||
plugin,
|
||||
} = await loadSite();
|
||||
const mock = jest
|
||||
.spyOn(cliDocs, 'cliDocsVersionCommand')
|
||||
.mockImplementation();
|
||||
|
@ -531,7 +577,8 @@ describe('versioned website (community)', () => {
|
|||
mock.mockRestore();
|
||||
});
|
||||
|
||||
test('getPathToWatch', () => {
|
||||
test('getPathToWatch', async () => {
|
||||
const {siteDir, plugin} = await loadSite();
|
||||
const pathToWatch = plugin.getPathsToWatch!();
|
||||
const matchPattern = pathToWatch.map((filepath) =>
|
||||
posixPath(path.relative(siteDir, filepath)),
|
||||
|
@ -540,8 +587,10 @@ describe('versioned website (community)', () => {
|
|||
expect(matchPattern).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"community_sidebars.json",
|
||||
"i18n/en/docusaurus-plugin-content-docs-community/current/**/*.{md,mdx}",
|
||||
"community/**/*.{md,mdx}",
|
||||
"community_versioned_sidebars/version-1.0.0-sidebars.json",
|
||||
"i18n/en/docusaurus-plugin-content-docs-community/version-1.0.0/**/*.{md,mdx}",
|
||||
"community_versioned_docs/version-1.0.0/**/*.{md,mdx}",
|
||||
]
|
||||
`);
|
||||
|
@ -568,6 +617,7 @@ describe('versioned website (community)', () => {
|
|||
});
|
||||
|
||||
test('content', async () => {
|
||||
const {siteDir, plugin, pluginContentDir} = await loadSite();
|
||||
const content = await plugin.loadContent!();
|
||||
expect(content.loadedVersions.length).toEqual(2);
|
||||
const [currentVersion, version100] = content.loadedVersions;
|
||||
|
@ -579,13 +629,17 @@ describe('versioned website (community)', () => {
|
|||
isDocsHomePage: false,
|
||||
permalink: '/community/next/team',
|
||||
slug: '/team',
|
||||
/*
|
||||
source: path.join(
|
||||
'@site',
|
||||
path.relative(siteDir, currentVersion.docsDirPath),
|
||||
'team.md',
|
||||
),
|
||||
title: 'team',
|
||||
description: 'Team current version',
|
||||
*/
|
||||
source:
|
||||
'@site/i18n/en/docusaurus-plugin-content-docs-community/current/team.md',
|
||||
title: 'Team title translated',
|
||||
description: 'Team current version (translated)',
|
||||
version: 'current',
|
||||
sidebar: 'community',
|
||||
});
|
||||
|
|
|
@ -11,6 +11,9 @@ import {
|
|||
collectSidebarDocItems,
|
||||
collectSidebarsDocIds,
|
||||
createSidebarsUtils,
|
||||
collectSidebarCategories,
|
||||
collectSidebarLinks,
|
||||
transformSidebarItems,
|
||||
} from '../sidebars';
|
||||
import {Sidebar, Sidebars} from '../types';
|
||||
|
||||
|
@ -163,7 +166,7 @@ describe('loadSidebars', () => {
|
|||
});
|
||||
|
||||
describe('collectSidebarDocItems', () => {
|
||||
test('can collect recursively', async () => {
|
||||
test('can collect docs', async () => {
|
||||
const sidebar: Sidebar = [
|
||||
{
|
||||
type: 'category',
|
||||
|
@ -213,7 +216,96 @@ describe('collectSidebarDocItems', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('collectSidebarsDocItems', () => {
|
||||
describe('collectSidebarCategories', () => {
|
||||
test('can collect categories', async () => {
|
||||
const sidebar: Sidebar = [
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
label: 'Category1',
|
||||
items: [
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
label: 'Subcategory 1',
|
||||
items: [{type: 'doc', id: 'doc1'}],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
label: 'Subcategory 2',
|
||||
items: [
|
||||
{type: 'doc', id: 'doc2'},
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
label: 'Sub sub category 1',
|
||||
items: [{type: 'doc', id: 'doc3'}],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
label: 'Category2',
|
||||
items: [
|
||||
{type: 'doc', id: 'doc4'},
|
||||
{type: 'doc', id: 'doc5'},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
expect(
|
||||
collectSidebarCategories(sidebar).map((category) => category.label),
|
||||
).toEqual([
|
||||
'Category1',
|
||||
'Subcategory 1',
|
||||
'Subcategory 2',
|
||||
'Sub sub category 1',
|
||||
'Category2',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collectSidebarLinks', () => {
|
||||
test('can collect links', async () => {
|
||||
const sidebar: Sidebar = [
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
label: 'Category1',
|
||||
items: [
|
||||
{
|
||||
type: 'link',
|
||||
href: 'https://google.com',
|
||||
label: 'Google',
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
label: 'Subcategory 2',
|
||||
items: [
|
||||
{
|
||||
type: 'link',
|
||||
href: 'https://facebook.com',
|
||||
label: 'Facebook',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
expect(collectSidebarLinks(sidebar).map((link) => link.href)).toEqual([
|
||||
'https://google.com',
|
||||
'https://facebook.com',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('collectSidebarsDocIds', () => {
|
||||
test('can collect sidebars doc items', async () => {
|
||||
const sidebar1: Sidebar = [
|
||||
{
|
||||
|
@ -256,6 +348,95 @@ describe('collectSidebarsDocItems', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('transformSidebarItems', () => {
|
||||
test('can transform sidebar items', async () => {
|
||||
const sidebar: Sidebar = [
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
label: 'Category1',
|
||||
items: [
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
label: 'Subcategory 1',
|
||||
items: [{type: 'doc', id: 'doc1'}],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
label: 'Subcategory 2',
|
||||
items: [
|
||||
{type: 'doc', id: 'doc2'},
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
label: 'Sub sub category 1',
|
||||
items: [{type: 'doc', id: 'doc3'}],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
label: 'Category2',
|
||||
items: [
|
||||
{type: 'doc', id: 'doc4'},
|
||||
{type: 'doc', id: 'doc5'},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
expect(
|
||||
transformSidebarItems(sidebar, (item) => {
|
||||
if (item.type === 'category') {
|
||||
return {...item, label: `MODIFIED LABEL: ${item.label}`};
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
label: 'MODIFIED LABEL: Category1',
|
||||
items: [
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
label: 'MODIFIED LABEL: Subcategory 1',
|
||||
items: [{type: 'doc', id: 'doc1'}],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
label: 'MODIFIED LABEL: Subcategory 2',
|
||||
items: [
|
||||
{type: 'doc', id: 'doc2'},
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
label: 'MODIFIED LABEL: Sub sub category 1',
|
||||
items: [{type: 'doc', id: 'doc3'}],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
collapsed: false,
|
||||
label: 'MODIFIED LABEL: Category2',
|
||||
items: [
|
||||
{type: 'doc', id: 'doc4'},
|
||||
{type: 'doc', id: 'doc5'},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSidebarsUtils', () => {
|
||||
const sidebar1: Sidebar = [
|
||||
{
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
/**
|
||||
* 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 {LoadedContent, DocMetadata, LoadedVersion} from '../types';
|
||||
import {CURRENT_VERSION_NAME} from '../constants';
|
||||
import {
|
||||
getLoadedContentTranslationFiles,
|
||||
translateLoadedContent,
|
||||
} from '../translations';
|
||||
import {updateTranslationFileMessages} from '@docusaurus/utils';
|
||||
|
||||
function createSampleDoc(doc: Pick<DocMetadata, 'id'>): DocMetadata {
|
||||
return {
|
||||
editUrl: 'any',
|
||||
isDocsHomePage: false,
|
||||
lastUpdatedAt: 0,
|
||||
lastUpdatedBy: 'any',
|
||||
next: undefined,
|
||||
previous: undefined,
|
||||
permalink: 'any',
|
||||
slug: 'any',
|
||||
source: 'any',
|
||||
unversionedId: 'any',
|
||||
version: 'any',
|
||||
title: `${doc.id} title`,
|
||||
sidebar_label: `${doc.id} title`,
|
||||
description: `${doc.id} description`,
|
||||
...doc,
|
||||
};
|
||||
}
|
||||
|
||||
function createSampleVersion(
|
||||
version: Pick<LoadedVersion, 'versionName'>,
|
||||
): LoadedVersion {
|
||||
return {
|
||||
versionLabel: `${version.versionName} label`,
|
||||
versionPath: '/docs/',
|
||||
mainDocId: '',
|
||||
permalinkToSidebar: {},
|
||||
routePriority: undefined,
|
||||
sidebarFilePath: 'any',
|
||||
isLast: true,
|
||||
docsDirPath: 'any',
|
||||
docsDirPathLocalized: 'any',
|
||||
docs: [
|
||||
createSampleDoc({
|
||||
id: 'doc1',
|
||||
}),
|
||||
createSampleDoc({
|
||||
id: 'doc2',
|
||||
}),
|
||||
createSampleDoc({
|
||||
id: 'doc3',
|
||||
}),
|
||||
createSampleDoc({
|
||||
id: 'doc4',
|
||||
}),
|
||||
createSampleDoc({
|
||||
id: 'doc5',
|
||||
}),
|
||||
],
|
||||
sidebars: {
|
||||
docs: [
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Getting started',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'doc1',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'doc2',
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
label: 'Link label',
|
||||
href: 'https://facebook.com',
|
||||
},
|
||||
{
|
||||
type: 'ref',
|
||||
id: 'doc1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'doc3',
|
||||
},
|
||||
],
|
||||
otherSidebar: [
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'doc4',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'doc5',
|
||||
},
|
||||
],
|
||||
},
|
||||
...version,
|
||||
};
|
||||
}
|
||||
|
||||
const SampleLoadedContent: LoadedContent = {
|
||||
loadedVersions: [
|
||||
createSampleVersion({
|
||||
versionName: CURRENT_VERSION_NAME,
|
||||
}),
|
||||
createSampleVersion({
|
||||
versionName: '2.0.0',
|
||||
}),
|
||||
createSampleVersion({
|
||||
versionName: '1.0.0',
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
function getSampleTranslationFiles() {
|
||||
return getLoadedContentTranslationFiles(SampleLoadedContent);
|
||||
}
|
||||
function getSampleTranslationFilesTranslated() {
|
||||
const translationFiles = getSampleTranslationFiles();
|
||||
return translationFiles.map((translationFile) =>
|
||||
updateTranslationFileMessages(
|
||||
translationFile,
|
||||
(message) => `${message} (translated)`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
describe('getLoadedContentTranslationFiles', () => {
|
||||
test('should return translation files matching snapshot', async () => {
|
||||
expect(getSampleTranslationFiles()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('translateLoadedContent', () => {
|
||||
test('should not translate anything if translation files are untranslated', () => {
|
||||
const translationFiles = getSampleTranslationFiles();
|
||||
expect(
|
||||
translateLoadedContent(SampleLoadedContent, translationFiles),
|
||||
).toEqual(SampleLoadedContent);
|
||||
});
|
||||
|
||||
test('should return translated loaded content matching snapshot', () => {
|
||||
const translationFiles = getSampleTranslationFilesTranslated();
|
||||
expect(
|
||||
translateLoadedContent(SampleLoadedContent, translationFiles),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -14,7 +14,14 @@ import {
|
|||
} from '../versions';
|
||||
import {DEFAULT_OPTIONS} from '../options';
|
||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
|
||||
import {VersionMetadata} from '../types';
|
||||
import {PluginOptions, VersionMetadata} from '../types';
|
||||
import {I18n} from '@docusaurus/types';
|
||||
|
||||
const DefaultI18N: I18n = {
|
||||
currentLocale: 'en',
|
||||
locales: ['en'],
|
||||
defaultLocale: 'en',
|
||||
};
|
||||
|
||||
describe('version paths', () => {
|
||||
test('getVersionedDocsDirPath', () => {
|
||||
|
@ -46,29 +53,39 @@ describe('version paths', () => {
|
|||
});
|
||||
|
||||
describe('simple site', () => {
|
||||
const simpleSiteDir = path.resolve(
|
||||
path.join(__dirname, '__fixtures__', 'simple-site'),
|
||||
);
|
||||
const defaultOptions = {
|
||||
id: DEFAULT_PLUGIN_ID,
|
||||
...DEFAULT_OPTIONS,
|
||||
};
|
||||
const defaultContext = {
|
||||
siteDir: simpleSiteDir,
|
||||
baseUrl: '/',
|
||||
};
|
||||
async function loadSite() {
|
||||
const simpleSiteDir = path.resolve(
|
||||
path.join(__dirname, '__fixtures__', 'simple-site'),
|
||||
);
|
||||
const defaultOptions: PluginOptions = {
|
||||
id: DEFAULT_PLUGIN_ID,
|
||||
...DEFAULT_OPTIONS,
|
||||
};
|
||||
const defaultContext = {
|
||||
siteDir: simpleSiteDir,
|
||||
baseUrl: '/',
|
||||
i18n: DefaultI18N,
|
||||
};
|
||||
|
||||
const vCurrent: VersionMetadata = {
|
||||
docsDirPath: path.join(simpleSiteDir, 'docs'),
|
||||
isLast: true,
|
||||
routePriority: -1,
|
||||
sidebarFilePath: path.join(simpleSiteDir, 'sidebars.json'),
|
||||
versionLabel: 'Next',
|
||||
versionName: 'current',
|
||||
versionPath: '/docs',
|
||||
};
|
||||
const vCurrent: VersionMetadata = {
|
||||
docsDirPath: path.join(simpleSiteDir, 'docs'),
|
||||
docsDirPathLocalized: path.join(
|
||||
simpleSiteDir,
|
||||
'i18n/en/docusaurus-plugin-content-docs/current',
|
||||
),
|
||||
isLast: true,
|
||||
routePriority: -1,
|
||||
sidebarFilePath: path.join(simpleSiteDir, 'sidebars.json'),
|
||||
versionLabel: 'Next',
|
||||
versionName: 'current',
|
||||
versionPath: '/docs',
|
||||
};
|
||||
return {simpleSiteDir, defaultOptions, defaultContext, vCurrent};
|
||||
}
|
||||
|
||||
test('readVersionsMetadata simple site', async () => {
|
||||
const {defaultOptions, defaultContext, vCurrent} = await loadSite();
|
||||
|
||||
test('readVersionsMetadata simple site', () => {
|
||||
const versionsMetadata = readVersionsMetadata({
|
||||
options: defaultOptions,
|
||||
context: defaultContext,
|
||||
|
@ -77,7 +94,9 @@ describe('simple site', () => {
|
|||
expect(versionsMetadata).toEqual([vCurrent]);
|
||||
});
|
||||
|
||||
test('readVersionsMetadata simple site with base url', () => {
|
||||
test('readVersionsMetadata simple site with base url', async () => {
|
||||
const {defaultOptions, defaultContext, vCurrent} = await loadSite();
|
||||
|
||||
const versionsMetadata = readVersionsMetadata({
|
||||
options: defaultOptions,
|
||||
context: {
|
||||
|
@ -94,7 +113,9 @@ describe('simple site', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('readVersionsMetadata simple site with current version config', () => {
|
||||
test('readVersionsMetadata simple site with current version config', async () => {
|
||||
const {defaultOptions, defaultContext, vCurrent} = await loadSite();
|
||||
|
||||
const versionsMetadata = readVersionsMetadata({
|
||||
options: {
|
||||
...defaultOptions,
|
||||
|
@ -121,7 +142,9 @@ describe('simple site', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('readVersionsMetadata simple site with unknown lastVersion should throw', () => {
|
||||
test('readVersionsMetadata simple site with unknown lastVersion should throw', async () => {
|
||||
const {defaultOptions, defaultContext} = await loadSite();
|
||||
|
||||
expect(() =>
|
||||
readVersionsMetadata({
|
||||
options: {...defaultOptions, lastVersion: 'unknownVersionName'},
|
||||
|
@ -132,7 +155,9 @@ describe('simple site', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('readVersionsMetadata simple site with unknown version configurations should throw', () => {
|
||||
test('readVersionsMetadata simple site with unknown version configurations should throw', async () => {
|
||||
const {defaultOptions, defaultContext} = await loadSite();
|
||||
|
||||
expect(() =>
|
||||
readVersionsMetadata({
|
||||
options: {
|
||||
|
@ -150,7 +175,9 @@ describe('simple site', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('readVersionsMetadata simple site with disableVersioning while single version should throw', () => {
|
||||
test('readVersionsMetadata simple site with disableVersioning while single version should throw', async () => {
|
||||
const {defaultOptions, defaultContext} = await loadSite();
|
||||
|
||||
expect(() =>
|
||||
readVersionsMetadata({
|
||||
options: {...defaultOptions, disableVersioning: true},
|
||||
|
@ -161,7 +188,9 @@ describe('simple site', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('readVersionsMetadata simple site without including current version should throw', () => {
|
||||
test('readVersionsMetadata simple site without including current version should throw', async () => {
|
||||
const {defaultOptions, defaultContext} = await loadSite();
|
||||
|
||||
expect(() =>
|
||||
readVersionsMetadata({
|
||||
options: {...defaultOptions, includeCurrentVersion: false},
|
||||
|
@ -174,71 +203,109 @@ describe('simple site', () => {
|
|||
});
|
||||
|
||||
describe('versioned site, pluginId=default', () => {
|
||||
const versionedSiteDir = path.resolve(
|
||||
path.join(__dirname, '__fixtures__', 'versioned-site'),
|
||||
);
|
||||
const defaultOptions = {
|
||||
id: DEFAULT_PLUGIN_ID,
|
||||
...DEFAULT_OPTIONS,
|
||||
};
|
||||
const defaultContext = {
|
||||
siteDir: versionedSiteDir,
|
||||
baseUrl: '/',
|
||||
};
|
||||
async function loadSite() {
|
||||
const versionedSiteDir = path.resolve(
|
||||
path.join(__dirname, '__fixtures__', 'versioned-site'),
|
||||
);
|
||||
const defaultOptions: PluginOptions = {
|
||||
id: DEFAULT_PLUGIN_ID,
|
||||
...DEFAULT_OPTIONS,
|
||||
};
|
||||
const defaultContext = {
|
||||
siteDir: versionedSiteDir,
|
||||
baseUrl: '/',
|
||||
i18n: DefaultI18N,
|
||||
};
|
||||
|
||||
const vCurrent: VersionMetadata = {
|
||||
docsDirPath: path.join(versionedSiteDir, 'docs'),
|
||||
isLast: false,
|
||||
routePriority: undefined,
|
||||
sidebarFilePath: path.join(versionedSiteDir, 'sidebars.json'),
|
||||
versionLabel: 'Next',
|
||||
versionName: 'current',
|
||||
versionPath: '/docs/next',
|
||||
};
|
||||
const vCurrent: VersionMetadata = {
|
||||
docsDirPath: path.join(versionedSiteDir, 'docs'),
|
||||
docsDirPathLocalized: path.join(
|
||||
versionedSiteDir,
|
||||
'i18n/en/docusaurus-plugin-content-docs/current',
|
||||
),
|
||||
isLast: false,
|
||||
routePriority: undefined,
|
||||
sidebarFilePath: path.join(versionedSiteDir, 'sidebars.json'),
|
||||
versionLabel: 'Next',
|
||||
versionName: 'current',
|
||||
versionPath: '/docs/next',
|
||||
};
|
||||
|
||||
const v101: VersionMetadata = {
|
||||
docsDirPath: path.join(versionedSiteDir, 'versioned_docs/version-1.0.1'),
|
||||
isLast: true,
|
||||
routePriority: -1,
|
||||
sidebarFilePath: path.join(
|
||||
const v101: VersionMetadata = {
|
||||
docsDirPath: path.join(versionedSiteDir, 'versioned_docs/version-1.0.1'),
|
||||
docsDirPathLocalized: path.join(
|
||||
versionedSiteDir,
|
||||
'i18n/en/docusaurus-plugin-content-docs/version-1.0.1',
|
||||
),
|
||||
isLast: true,
|
||||
routePriority: -1,
|
||||
sidebarFilePath: path.join(
|
||||
versionedSiteDir,
|
||||
'versioned_sidebars/version-1.0.1-sidebars.json',
|
||||
),
|
||||
versionLabel: '1.0.1',
|
||||
versionName: '1.0.1',
|
||||
versionPath: '/docs',
|
||||
};
|
||||
|
||||
const v100: VersionMetadata = {
|
||||
docsDirPath: path.join(versionedSiteDir, 'versioned_docs/version-1.0.0'),
|
||||
docsDirPathLocalized: path.join(
|
||||
versionedSiteDir,
|
||||
'i18n/en/docusaurus-plugin-content-docs/version-1.0.0',
|
||||
),
|
||||
isLast: false,
|
||||
routePriority: undefined,
|
||||
sidebarFilePath: path.join(
|
||||
versionedSiteDir,
|
||||
'versioned_sidebars/version-1.0.0-sidebars.json',
|
||||
),
|
||||
versionLabel: '1.0.0',
|
||||
versionName: '1.0.0',
|
||||
versionPath: '/docs/1.0.0',
|
||||
};
|
||||
|
||||
const vwithSlugs: VersionMetadata = {
|
||||
docsDirPath: path.join(
|
||||
versionedSiteDir,
|
||||
'versioned_docs/version-withSlugs',
|
||||
),
|
||||
docsDirPathLocalized: path.join(
|
||||
versionedSiteDir,
|
||||
'i18n/en/docusaurus-plugin-content-docs/version-withSlugs',
|
||||
),
|
||||
isLast: false,
|
||||
routePriority: undefined,
|
||||
sidebarFilePath: path.join(
|
||||
versionedSiteDir,
|
||||
'versioned_sidebars/version-withSlugs-sidebars.json',
|
||||
),
|
||||
versionLabel: 'withSlugs',
|
||||
versionName: 'withSlugs',
|
||||
versionPath: '/docs/withSlugs',
|
||||
};
|
||||
|
||||
return {
|
||||
versionedSiteDir,
|
||||
'versioned_sidebars/version-1.0.1-sidebars.json',
|
||||
),
|
||||
versionLabel: '1.0.1',
|
||||
versionName: '1.0.1',
|
||||
versionPath: '/docs',
|
||||
};
|
||||
defaultOptions,
|
||||
defaultContext,
|
||||
vCurrent,
|
||||
v101,
|
||||
v100,
|
||||
vwithSlugs,
|
||||
};
|
||||
}
|
||||
|
||||
const v100: VersionMetadata = {
|
||||
docsDirPath: path.join(versionedSiteDir, 'versioned_docs/version-1.0.0'),
|
||||
isLast: false,
|
||||
routePriority: undefined,
|
||||
sidebarFilePath: path.join(
|
||||
versionedSiteDir,
|
||||
'versioned_sidebars/version-1.0.0-sidebars.json',
|
||||
),
|
||||
versionLabel: '1.0.0',
|
||||
versionName: '1.0.0',
|
||||
versionPath: '/docs/1.0.0',
|
||||
};
|
||||
test('readVersionsMetadata versioned site', async () => {
|
||||
const {
|
||||
defaultOptions,
|
||||
defaultContext,
|
||||
vCurrent,
|
||||
v101,
|
||||
v100,
|
||||
vwithSlugs,
|
||||
} = await loadSite();
|
||||
|
||||
const vwithSlugs: VersionMetadata = {
|
||||
docsDirPath: path.join(
|
||||
versionedSiteDir,
|
||||
'versioned_docs/version-withSlugs',
|
||||
),
|
||||
isLast: false,
|
||||
routePriority: undefined,
|
||||
sidebarFilePath: path.join(
|
||||
versionedSiteDir,
|
||||
'versioned_sidebars/version-withSlugs-sidebars.json',
|
||||
),
|
||||
versionLabel: 'withSlugs',
|
||||
versionName: 'withSlugs',
|
||||
versionPath: '/docs/withSlugs',
|
||||
};
|
||||
|
||||
test('readVersionsMetadata versioned site', () => {
|
||||
const versionsMetadata = readVersionsMetadata({
|
||||
options: defaultOptions,
|
||||
context: defaultContext,
|
||||
|
@ -247,7 +314,15 @@ describe('versioned site, pluginId=default', () => {
|
|||
expect(versionsMetadata).toEqual([vCurrent, v101, v100, vwithSlugs]);
|
||||
});
|
||||
|
||||
test('readVersionsMetadata versioned site with includeCurrentVersion=false', () => {
|
||||
test('readVersionsMetadata versioned site with includeCurrentVersion=false', async () => {
|
||||
const {
|
||||
defaultOptions,
|
||||
defaultContext,
|
||||
v101,
|
||||
v100,
|
||||
vwithSlugs,
|
||||
} = await loadSite();
|
||||
|
||||
const versionsMetadata = readVersionsMetadata({
|
||||
options: {...defaultOptions, includeCurrentVersion: false},
|
||||
context: defaultContext,
|
||||
|
@ -261,7 +336,16 @@ describe('versioned site, pluginId=default', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('readVersionsMetadata versioned site with version options', () => {
|
||||
test('readVersionsMetadata versioned site with version options', async () => {
|
||||
const {
|
||||
defaultOptions,
|
||||
defaultContext,
|
||||
vCurrent,
|
||||
v101,
|
||||
v100,
|
||||
vwithSlugs,
|
||||
} = await loadSite();
|
||||
|
||||
const versionsMetadata = readVersionsMetadata({
|
||||
options: {
|
||||
...defaultOptions,
|
||||
|
@ -297,7 +381,9 @@ describe('versioned site, pluginId=default', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('readVersionsMetadata versioned site with onlyIncludeVersions option', () => {
|
||||
test('readVersionsMetadata versioned site with onlyIncludeVersions option', async () => {
|
||||
const {defaultOptions, defaultContext, v101, vwithSlugs} = await loadSite();
|
||||
|
||||
const versionsMetadata = readVersionsMetadata({
|
||||
options: {
|
||||
...defaultOptions,
|
||||
|
@ -310,7 +396,9 @@ describe('versioned site, pluginId=default', () => {
|
|||
expect(versionsMetadata).toEqual([v101, vwithSlugs]);
|
||||
});
|
||||
|
||||
test('readVersionsMetadata versioned site with disableVersioning', () => {
|
||||
test('readVersionsMetadata versioned site with disableVersioning', async () => {
|
||||
const {defaultOptions, defaultContext, vCurrent} = await loadSite();
|
||||
|
||||
const versionsMetadata = readVersionsMetadata({
|
||||
options: {...defaultOptions, disableVersioning: true},
|
||||
context: defaultContext,
|
||||
|
@ -321,7 +409,9 @@ describe('versioned site, pluginId=default', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('readVersionsMetadata versioned site with all versions disabled', () => {
|
||||
test('readVersionsMetadata versioned site with all versions disabled', async () => {
|
||||
const {defaultOptions, defaultContext} = await loadSite();
|
||||
|
||||
expect(() =>
|
||||
readVersionsMetadata({
|
||||
options: {
|
||||
|
@ -336,7 +426,9 @@ describe('versioned site, pluginId=default', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('readVersionsMetadata versioned site with empty onlyIncludeVersions', () => {
|
||||
test('readVersionsMetadata versioned site with empty onlyIncludeVersions', async () => {
|
||||
const {defaultOptions, defaultContext} = await loadSite();
|
||||
|
||||
expect(() =>
|
||||
readVersionsMetadata({
|
||||
options: {
|
||||
|
@ -350,7 +442,9 @@ describe('versioned site, pluginId=default', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('readVersionsMetadata versioned site with unknown versions in onlyIncludeVersions', () => {
|
||||
test('readVersionsMetadata versioned site with unknown versions in onlyIncludeVersions', async () => {
|
||||
const {defaultOptions, defaultContext} = await loadSite();
|
||||
|
||||
expect(() =>
|
||||
readVersionsMetadata({
|
||||
options: {
|
||||
|
@ -364,7 +458,9 @@ describe('versioned site, pluginId=default', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('readVersionsMetadata versioned site with lastVersion not in onlyIncludeVersions', () => {
|
||||
test('readVersionsMetadata versioned site with lastVersion not in onlyIncludeVersions', async () => {
|
||||
const {defaultOptions, defaultContext} = await loadSite();
|
||||
|
||||
expect(() =>
|
||||
readVersionsMetadata({
|
||||
options: {
|
||||
|
@ -379,7 +475,9 @@ describe('versioned site, pluginId=default', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('readVersionsMetadata versioned site with invalid versions.json file', () => {
|
||||
test('readVersionsMetadata versioned site with invalid versions.json file', async () => {
|
||||
const {defaultOptions, defaultContext} = await loadSite();
|
||||
|
||||
const mock = jest.spyOn(JSON, 'parse').mockImplementationOnce(() => {
|
||||
return {
|
||||
invalid: 'json',
|
||||
|
@ -399,47 +497,62 @@ describe('versioned site, pluginId=default', () => {
|
|||
});
|
||||
|
||||
describe('versioned site, pluginId=community', () => {
|
||||
const versionedSiteDir = path.resolve(
|
||||
path.join(__dirname, '__fixtures__', 'versioned-site'),
|
||||
);
|
||||
const defaultOptions = {
|
||||
...DEFAULT_OPTIONS,
|
||||
id: 'community',
|
||||
path: 'community',
|
||||
routeBasePath: 'communityBasePath',
|
||||
};
|
||||
const defaultContext = {
|
||||
siteDir: versionedSiteDir,
|
||||
baseUrl: '/',
|
||||
};
|
||||
async function loadSite() {
|
||||
const versionedSiteDir = path.resolve(
|
||||
path.join(__dirname, '__fixtures__', 'versioned-site'),
|
||||
);
|
||||
const defaultOptions: PluginOptions = {
|
||||
...DEFAULT_OPTIONS,
|
||||
id: 'community',
|
||||
path: 'community',
|
||||
routeBasePath: 'communityBasePath',
|
||||
};
|
||||
const defaultContext = {
|
||||
siteDir: versionedSiteDir,
|
||||
baseUrl: '/',
|
||||
i18n: DefaultI18N,
|
||||
};
|
||||
|
||||
const vCurrent: VersionMetadata = {
|
||||
docsDirPath: path.join(versionedSiteDir, 'community'),
|
||||
isLast: false,
|
||||
routePriority: undefined,
|
||||
sidebarFilePath: path.join(versionedSiteDir, 'sidebars.json'),
|
||||
versionLabel: 'Next',
|
||||
versionName: 'current',
|
||||
versionPath: '/communityBasePath/next',
|
||||
};
|
||||
const vCurrent: VersionMetadata = {
|
||||
docsDirPath: path.join(versionedSiteDir, 'community'),
|
||||
docsDirPathLocalized: path.join(
|
||||
versionedSiteDir,
|
||||
'i18n/en/docusaurus-plugin-content-docs-community/current',
|
||||
),
|
||||
isLast: false,
|
||||
routePriority: undefined,
|
||||
sidebarFilePath: path.join(versionedSiteDir, 'sidebars.json'),
|
||||
versionLabel: 'Next',
|
||||
versionName: 'current',
|
||||
versionPath: '/communityBasePath/next',
|
||||
};
|
||||
|
||||
const v100: VersionMetadata = {
|
||||
docsDirPath: path.join(
|
||||
versionedSiteDir,
|
||||
'community_versioned_docs/version-1.0.0',
|
||||
),
|
||||
isLast: true,
|
||||
routePriority: -1,
|
||||
sidebarFilePath: path.join(
|
||||
versionedSiteDir,
|
||||
'community_versioned_sidebars/version-1.0.0-sidebars.json',
|
||||
),
|
||||
versionLabel: '1.0.0',
|
||||
versionName: '1.0.0',
|
||||
versionPath: '/communityBasePath',
|
||||
};
|
||||
const v100: VersionMetadata = {
|
||||
docsDirPath: path.join(
|
||||
versionedSiteDir,
|
||||
'community_versioned_docs/version-1.0.0',
|
||||
),
|
||||
docsDirPathLocalized: path.join(
|
||||
versionedSiteDir,
|
||||
'i18n/en/docusaurus-plugin-content-docs-community/version-1.0.0',
|
||||
),
|
||||
isLast: true,
|
||||
routePriority: -1,
|
||||
sidebarFilePath: path.join(
|
||||
versionedSiteDir,
|
||||
'community_versioned_sidebars/version-1.0.0-sidebars.json',
|
||||
),
|
||||
versionLabel: '1.0.0',
|
||||
versionName: '1.0.0',
|
||||
versionPath: '/communityBasePath',
|
||||
};
|
||||
|
||||
return {versionedSiteDir, defaultOptions, defaultContext, vCurrent, v100};
|
||||
}
|
||||
|
||||
test('readVersionsMetadata versioned site (community)', async () => {
|
||||
const {defaultOptions, defaultContext, vCurrent, v100} = await loadSite();
|
||||
|
||||
test('readVersionsMetadata versioned site (community)', () => {
|
||||
const versionsMetadata = readVersionsMetadata({
|
||||
options: defaultOptions,
|
||||
context: defaultContext,
|
||||
|
@ -448,7 +561,9 @@ describe('versioned site, pluginId=community', () => {
|
|||
expect(versionsMetadata).toEqual([vCurrent, v100]);
|
||||
});
|
||||
|
||||
test('readVersionsMetadata versioned site (community) with includeCurrentVersion=false', () => {
|
||||
test('readVersionsMetadata versioned site (community) with includeCurrentVersion=false', async () => {
|
||||
const {defaultOptions, defaultContext, v100} = await loadSite();
|
||||
|
||||
const versionsMetadata = readVersionsMetadata({
|
||||
options: {...defaultOptions, includeCurrentVersion: false},
|
||||
context: defaultContext,
|
||||
|
@ -460,7 +575,9 @@ describe('versioned site, pluginId=community', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('readVersionsMetadata versioned site (community) with disableVersioning', () => {
|
||||
test('readVersionsMetadata versioned site (community) with disableVersioning', async () => {
|
||||
const {defaultOptions, defaultContext, vCurrent} = await loadSite();
|
||||
|
||||
const versionsMetadata = readVersionsMetadata({
|
||||
options: {...defaultOptions, disableVersioning: true},
|
||||
context: defaultContext,
|
||||
|
@ -476,7 +593,9 @@ describe('versioned site, pluginId=community', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('readVersionsMetadata versioned site (community) with all versions disabled', () => {
|
||||
test('readVersionsMetadata versioned site (community) with all versions disabled', async () => {
|
||||
const {defaultOptions, defaultContext} = await loadSite();
|
||||
|
||||
expect(() =>
|
||||
readVersionsMetadata({
|
||||
options: {
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
normalizeUrl,
|
||||
getEditUrl,
|
||||
parseMarkdownString,
|
||||
getFolderContainingFile,
|
||||
} from '@docusaurus/utils';
|
||||
import {LoadContext} from '@docusaurus/types';
|
||||
|
||||
|
@ -27,6 +28,7 @@ import {
|
|||
import getSlug from './slug';
|
||||
import {CURRENT_VERSION_NAME} from './constants';
|
||||
import globby from 'globby';
|
||||
import {getDocsDirPaths} from './versions';
|
||||
|
||||
type LastUpdateOptions = Pick<
|
||||
PluginOptions,
|
||||
|
@ -61,16 +63,25 @@ async function readLastUpdateData(
|
|||
}
|
||||
|
||||
export async function readDocFile(
|
||||
docsDirPath: string,
|
||||
versionMetadata: Pick<
|
||||
VersionMetadata,
|
||||
'docsDirPath' | 'docsDirPathLocalized'
|
||||
>,
|
||||
source: string,
|
||||
options: LastUpdateOptions,
|
||||
): Promise<DocFile> {
|
||||
const filePath = path.join(docsDirPath, source);
|
||||
const folderPath = await getFolderContainingFile(
|
||||
getDocsDirPaths(versionMetadata),
|
||||
source,
|
||||
);
|
||||
|
||||
const filePath = path.join(folderPath, source);
|
||||
|
||||
const [content, lastUpdate] = await Promise.all([
|
||||
fs.readFile(filePath, 'utf-8'),
|
||||
readLastUpdateData(filePath, options),
|
||||
]);
|
||||
return {source, content, lastUpdate};
|
||||
return {source, content, lastUpdate, filePath};
|
||||
}
|
||||
|
||||
export async function readVersionDocs(
|
||||
|
@ -84,9 +95,7 @@ export async function readVersionDocs(
|
|||
cwd: versionMetadata.docsDirPath,
|
||||
});
|
||||
return Promise.all(
|
||||
sources.map((source) =>
|
||||
readDocFile(versionMetadata.docsDirPath, source, options),
|
||||
),
|
||||
sources.map((source) => readDocFile(versionMetadata, source, options)),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -101,10 +110,9 @@ export function processDocMetadata({
|
|||
context: LoadContext;
|
||||
options: MetadataOptions;
|
||||
}): DocMetadataBase {
|
||||
const {source, content, lastUpdate} = docFile;
|
||||
const {source, content, lastUpdate, filePath} = docFile;
|
||||
const {editUrl, homePageId} = options;
|
||||
const {siteDir} = context;
|
||||
const filePath = path.join(versionMetadata.docsDirPath, source);
|
||||
|
||||
// ex: api/myDoc -> api
|
||||
// ex: myDoc -> .
|
||||
|
|
|
@ -21,7 +21,7 @@ import {LoadContext, Plugin, RouteConfig} from '@docusaurus/types';
|
|||
|
||||
import {loadSidebars, createSidebarsUtils} from './sidebars';
|
||||
import {readVersionDocs, processDocMetadata} from './docs';
|
||||
import {readVersionsMetadata} from './versions';
|
||||
import {getDocsDirPaths, readVersionsMetadata} from './versions';
|
||||
|
||||
import {
|
||||
PluginOptions,
|
||||
|
@ -44,6 +44,10 @@ import {OptionsSchema} from './options';
|
|||
import {flatten, keyBy, compact} from 'lodash';
|
||||
import {toGlobalDataVersion} from './globalData';
|
||||
import {toVersionMetadataProp} from './props';
|
||||
import {
|
||||
translateLoadedContent,
|
||||
getLoadedContentTranslationFiles,
|
||||
} from './translations';
|
||||
|
||||
export default function pluginContentDocs(
|
||||
context: LoadContext,
|
||||
|
@ -99,6 +103,10 @@ export default function pluginContentDocs(
|
|||
});
|
||||
},
|
||||
|
||||
async getTranslationFiles() {
|
||||
return getLoadedContentTranslationFiles(await this.loadContent!());
|
||||
},
|
||||
|
||||
getClientModules() {
|
||||
const modules = [];
|
||||
if (options.admonitions) {
|
||||
|
@ -111,8 +119,12 @@ export default function pluginContentDocs(
|
|||
function getVersionPathsToWatch(version: VersionMetadata): string[] {
|
||||
return [
|
||||
version.sidebarFilePath,
|
||||
...options.include.map(
|
||||
(pattern) => `${version.docsDirPath}/${pattern}`,
|
||||
...flatten(
|
||||
options.include.map((pattern) =>
|
||||
getDocsDirPaths(version).map(
|
||||
(docsDirPath) => `${docsDirPath}/${pattern}`,
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
@ -235,6 +247,10 @@ export default function pluginContentDocs(
|
|||
};
|
||||
},
|
||||
|
||||
translateContent({content, translationFiles}) {
|
||||
return translateLoadedContent(content, translationFiles);
|
||||
},
|
||||
|
||||
async contentLoaded({content, actions}) {
|
||||
const {loadedVersions} = content;
|
||||
const {docLayoutComponent, docItemComponent} = options;
|
||||
|
@ -318,7 +334,6 @@ export default function pluginContentDocs(
|
|||
if (siteConfig.onBrokenMarkdownLinks === 'ignore') {
|
||||
return;
|
||||
}
|
||||
|
||||
reportMessage(
|
||||
`Docs markdown link couldn't be resolved: (${brokenMarkdownLink.link}) in ${brokenMarkdownLink.filePath} for version ${brokenMarkdownLink.version.versionName}`,
|
||||
siteConfig.onBrokenMarkdownLinks,
|
||||
|
@ -329,7 +344,7 @@ export default function pluginContentDocs(
|
|||
function createMDXLoaderRule(): RuleSetRule {
|
||||
return {
|
||||
test: /(\.mdx?)$/,
|
||||
include: versionsMetadata.map((vmd) => vmd.docsDirPath),
|
||||
include: flatten(versionsMetadata.map(getDocsDirPaths)),
|
||||
use: compact([
|
||||
getCacheLoader(isServer),
|
||||
getBabelLoader(isServer),
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
### localized doc
|
|
@ -8,3 +8,5 @@
|
|||
|
||||
- [doc1](doc1.md)
|
||||
- [doc2](./doc2.md)
|
||||
|
||||
- [doc-localized](./doc-localized.md)
|
||||
|
|
|
@ -35,6 +35,8 @@ exports[`transform to correct links 1`] = `
|
|||
|
||||
- [doc1](/docs/doc1)
|
||||
- [doc2](/docs/doc2)
|
||||
|
||||
- [doc-localized](/fr/doc-localized)
|
||||
"
|
||||
`;
|
||||
|
||||
|
|
|
@ -16,15 +16,21 @@ import {
|
|||
} from '../../types';
|
||||
import {VERSIONED_DOCS_DIR, CURRENT_VERSION_NAME} from '../../constants';
|
||||
|
||||
function createFakeVersion(
|
||||
versionName: string,
|
||||
docsDirPath: string,
|
||||
): VersionMetadata {
|
||||
function createFakeVersion({
|
||||
versionName,
|
||||
docsDirPath,
|
||||
docsDirPathLocalized,
|
||||
}: {
|
||||
versionName: string;
|
||||
docsDirPath: string;
|
||||
docsDirPathLocalized: string;
|
||||
}): VersionMetadata {
|
||||
return {
|
||||
versionName,
|
||||
versionLabel: 'Any',
|
||||
versionPath: 'any',
|
||||
docsDirPath,
|
||||
docsDirPathLocalized,
|
||||
sidebarFilePath: 'any',
|
||||
routePriority: undefined,
|
||||
isLast: false,
|
||||
|
@ -33,14 +39,29 @@ function createFakeVersion(
|
|||
|
||||
const siteDir = path.join(__dirname, '__fixtures__');
|
||||
|
||||
const versionCurrent = createFakeVersion(
|
||||
CURRENT_VERSION_NAME,
|
||||
path.join(siteDir, 'docs'),
|
||||
);
|
||||
const version100 = createFakeVersion(
|
||||
CURRENT_VERSION_NAME,
|
||||
path.join(siteDir, VERSIONED_DOCS_DIR, 'version-1.0.0'),
|
||||
);
|
||||
const versionCurrent = createFakeVersion({
|
||||
versionName: CURRENT_VERSION_NAME,
|
||||
docsDirPath: path.join(siteDir, 'docs'),
|
||||
docsDirPathLocalized: path.join(
|
||||
siteDir,
|
||||
'i18n',
|
||||
'fr',
|
||||
'docusaurus-plugin-content-docs',
|
||||
CURRENT_VERSION_NAME,
|
||||
),
|
||||
});
|
||||
|
||||
const version100 = createFakeVersion({
|
||||
versionName: '1.0.0',
|
||||
docsDirPath: path.join(siteDir, VERSIONED_DOCS_DIR, 'version-1.0.0'),
|
||||
docsDirPathLocalized: path.join(
|
||||
siteDir,
|
||||
'i18n',
|
||||
'fr',
|
||||
'docusaurus-plugin-content-docs',
|
||||
'version-1.0.0',
|
||||
),
|
||||
});
|
||||
|
||||
const sourceToPermalink: SourceToPermalink = {
|
||||
'@site/docs/doc1.md': '/docs/doc1',
|
||||
|
@ -50,6 +71,10 @@ const sourceToPermalink: SourceToPermalink = {
|
|||
'@site/versioned_docs/version-1.0.0/doc2.md': '/docs/1.0.0/doc2',
|
||||
'@site/versioned_docs/version-1.0.0/subdir/doc1.md':
|
||||
'/docs/1.0.0/subdir/doc1',
|
||||
|
||||
'@site/i18n/fr/docusaurus-plugin-content-docs/current/doc-localized.md':
|
||||
'/fr/doc-localized',
|
||||
'@site/docs/doc-localized': '/doc-localized',
|
||||
};
|
||||
|
||||
function createMarkdownOptions(
|
||||
|
@ -85,9 +110,11 @@ test('transform to correct links', () => {
|
|||
expect(transformedContent).toContain('](/docs/doc1');
|
||||
expect(transformedContent).toContain('](/docs/doc2');
|
||||
expect(transformedContent).toContain('](/docs/subdir/doc3');
|
||||
expect(transformedContent).toContain('](/fr/doc-localized');
|
||||
expect(transformedContent).not.toContain('](doc1.md)');
|
||||
expect(transformedContent).not.toContain('](./doc2.md)');
|
||||
expect(transformedContent).not.toContain('](subdir/doc3.md)');
|
||||
expect(transformedContent).not.toContain('](/doc-localized');
|
||||
expect(content).not.toEqual(transformedContent);
|
||||
});
|
||||
|
||||
|
|
|
@ -12,10 +12,13 @@ import {
|
|||
VersionMetadata,
|
||||
BrokenMarkdownLink,
|
||||
} from '../types';
|
||||
import {getDocsDirPaths} from '../versions';
|
||||
|
||||
function getVersion(filePath: string, options: DocsMarkdownOption) {
|
||||
const versionFound = options.versionsMetadata.find((version) =>
|
||||
filePath.startsWith(version.docsDirPath),
|
||||
getDocsDirPaths(version).some((docsDirPath) =>
|
||||
filePath.startsWith(docsDirPath),
|
||||
),
|
||||
);
|
||||
if (!versionFound) {
|
||||
throw new Error(
|
||||
|
@ -32,7 +35,7 @@ function replaceMarkdownLinks(
|
|||
options: DocsMarkdownOption,
|
||||
) {
|
||||
const {siteDir, sourceToPermalink, onBrokenMarkdownLink} = options;
|
||||
const {docsDirPath} = version;
|
||||
const {docsDirPath, docsDirPathLocalized} = version;
|
||||
|
||||
// Replace internal markdown linking (except in fenced blocks).
|
||||
let fencedBlock = false;
|
||||
|
@ -53,12 +56,15 @@ function replaceMarkdownLinks(
|
|||
while (mdMatch !== null) {
|
||||
// Replace it to correct html link.
|
||||
const mdLink = mdMatch[1];
|
||||
const targetSource = `${docsDirPath}/${mdLink}`;
|
||||
|
||||
const aliasedSource = (source: string) =>
|
||||
`@site/${path.relative(siteDir, source)}`;
|
||||
|
||||
const permalink =
|
||||
sourceToPermalink[aliasedSource(resolve(filePath, mdLink))] ||
|
||||
sourceToPermalink[aliasedSource(targetSource)];
|
||||
sourceToPermalink[aliasedSource(`${docsDirPathLocalized}/${mdLink}`)] ||
|
||||
sourceToPermalink[aliasedSource(`${docsDirPath}/${mdLink}`)];
|
||||
|
||||
if (permalink) {
|
||||
modifiedLine = modifiedLine.replace(mdLink, permalink);
|
||||
} else {
|
||||
|
|
|
@ -14,6 +14,8 @@ import {
|
|||
SidebarItemLink,
|
||||
SidebarItemDoc,
|
||||
Sidebar,
|
||||
SidebarItemCategory,
|
||||
SidebarItemType,
|
||||
} from './types';
|
||||
import {mapValues, flatten, difference} from 'lodash';
|
||||
import {getElementsAround} from '@docusaurus/utils';
|
||||
|
@ -213,25 +215,51 @@ export function loadSidebars(sidebarFilePath: string): Sidebars {
|
|||
return normalizeSidebars(sidebarJson);
|
||||
}
|
||||
|
||||
// traverse the sidebar tree in depth to find all doc items, in correct order
|
||||
export function collectSidebarDocItems(sidebar: Sidebar): SidebarItemDoc[] {
|
||||
function collectRecursive(item: SidebarItem): SidebarItemDoc[] {
|
||||
if (item.type === 'doc') {
|
||||
return [item];
|
||||
}
|
||||
if (item.type === 'category') {
|
||||
return flatten(item.items.map(collectRecursive));
|
||||
}
|
||||
// Refs and links should not be shown in navigation.
|
||||
if (item.type === 'ref' || item.type === 'link') {
|
||||
return [];
|
||||
}
|
||||
throw new Error(`unknown sidebar item type = ${item.type}`);
|
||||
function collectSidebarItemsOfType<
|
||||
Type extends SidebarItemType,
|
||||
Item extends SidebarItem & {type: SidebarItemType}
|
||||
>(type: Type, sidebar: Sidebar): Item[] {
|
||||
function collectRecursive(item: SidebarItem): Item[] {
|
||||
const currentItemsCollected: Item[] =
|
||||
item.type === type ? [item as Item] : [];
|
||||
|
||||
const childItemsCollected: Item[] =
|
||||
item.type === 'category' ? flatten(item.items.map(collectRecursive)) : [];
|
||||
|
||||
return [...currentItemsCollected, ...childItemsCollected];
|
||||
}
|
||||
|
||||
return flatten(sidebar.map(collectRecursive));
|
||||
}
|
||||
|
||||
export function collectSidebarDocItems(sidebar: Sidebar): SidebarItemDoc[] {
|
||||
return collectSidebarItemsOfType('doc', sidebar);
|
||||
}
|
||||
export function collectSidebarCategories(
|
||||
sidebar: Sidebar,
|
||||
): SidebarItemCategory[] {
|
||||
return collectSidebarItemsOfType('category', sidebar);
|
||||
}
|
||||
export function collectSidebarLinks(sidebar: Sidebar): SidebarItemLink[] {
|
||||
return collectSidebarItemsOfType('link', sidebar);
|
||||
}
|
||||
|
||||
export function transformSidebarItems(
|
||||
sidebar: Sidebar,
|
||||
updateFn: (item: SidebarItem) => SidebarItem,
|
||||
): Sidebar {
|
||||
function transformRecursive(item: SidebarItem): SidebarItem {
|
||||
if (item.type === 'category') {
|
||||
return updateFn({
|
||||
...item,
|
||||
items: item.items.map(transformRecursive),
|
||||
});
|
||||
}
|
||||
return updateFn(item);
|
||||
}
|
||||
return sidebar.map(transformRecursive);
|
||||
}
|
||||
|
||||
export function collectSidebarsDocIds(
|
||||
sidebars: Sidebars,
|
||||
): Record<string, string[]> {
|
||||
|
|
259
packages/docusaurus-plugin-content-docs/src/translations.ts
Normal file
259
packages/docusaurus-plugin-content-docs/src/translations.ts
Normal file
|
@ -0,0 +1,259 @@
|
|||
/**
|
||||
* 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 {
|
||||
LoadedVersion,
|
||||
Sidebar,
|
||||
LoadedContent,
|
||||
Sidebars,
|
||||
SidebarItem,
|
||||
} from './types';
|
||||
|
||||
import {chain, mapValues, flatten, keyBy} from 'lodash';
|
||||
import {
|
||||
collectSidebarCategories,
|
||||
transformSidebarItems,
|
||||
collectSidebarLinks,
|
||||
} from './sidebars';
|
||||
import {
|
||||
TranslationFileContent,
|
||||
TranslationFile,
|
||||
TranslationFiles,
|
||||
} from '@docusaurus/types';
|
||||
import {mergeTranslations} from '@docusaurus/utils';
|
||||
import {CURRENT_VERSION_NAME} from './constants';
|
||||
|
||||
function getVersionFileName(versionName: string): string {
|
||||
if (versionName === CURRENT_VERSION_NAME) {
|
||||
return versionName;
|
||||
} else {
|
||||
// I don't like this "version-" prefix,
|
||||
// but it's for consistency with site/versioned_docs
|
||||
return `version-${versionName}`;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO legacy, the sidebar name is like "version-2.0.0-alpha.66/docs"
|
||||
// input: "version-2.0.0-alpha.66/docs"
|
||||
// output: "docs"
|
||||
function getNormalizedSidebarName({
|
||||
versionName,
|
||||
sidebarName,
|
||||
}: {
|
||||
versionName: string;
|
||||
sidebarName: string;
|
||||
}): string {
|
||||
if (versionName === CURRENT_VERSION_NAME || !sidebarName.includes('/')) {
|
||||
return sidebarName;
|
||||
}
|
||||
const [, ...rest] = sidebarName.split('/');
|
||||
return rest.join('/');
|
||||
}
|
||||
|
||||
/*
|
||||
// Do we need to translate doc metadatas?
|
||||
// It seems translating frontmatter labels is good enough
|
||||
function getDocTranslations(doc: DocMetadata): TranslationFileContent {
|
||||
return {
|
||||
[`${doc.unversionedId}.title`]: {
|
||||
message: doc.title,
|
||||
description: `The title for doc with id=${doc.unversionedId}`,
|
||||
},
|
||||
...(doc.sidebar_label
|
||||
? {
|
||||
[`${doc.unversionedId}.sidebar_label`]: {
|
||||
message: doc.sidebar_label,
|
||||
description: `The sidebar label for doc with id=${doc.unversionedId}`,
|
||||
},
|
||||
}
|
||||
: undefined),
|
||||
};
|
||||
}
|
||||
function translateDoc(
|
||||
doc: DocMetadata,
|
||||
docsTranslations: TranslationFileContent,
|
||||
): DocMetadata {
|
||||
return {
|
||||
...doc,
|
||||
title: docsTranslations[`${doc.unversionedId}.title`]?.message ?? doc.title,
|
||||
sidebar_label:
|
||||
docsTranslations[`${doc.unversionedId}.sidebar_label`]?.message ??
|
||||
doc.sidebar_label,
|
||||
};
|
||||
}
|
||||
|
||||
function getDocsTranslations(version: LoadedVersion): TranslationFileContent {
|
||||
return mergeTranslations(version.docs.map(getDocTranslations));
|
||||
}
|
||||
function translateDocs(
|
||||
docs: DocMetadata[],
|
||||
docsTranslations: TranslationFileContent,
|
||||
): DocMetadata[] {
|
||||
return docs.map((doc) => translateDoc(doc, docsTranslations));
|
||||
}
|
||||
*/
|
||||
|
||||
function getSidebarTranslationFileContent(
|
||||
sidebar: Sidebar,
|
||||
sidebarName: string,
|
||||
): TranslationFileContent {
|
||||
const categories = collectSidebarCategories(sidebar);
|
||||
const categoryContent: TranslationFileContent = chain(categories)
|
||||
.keyBy((category) => `sidebar.${sidebarName}.category.${category.label}`)
|
||||
.mapValues((category) => ({
|
||||
message: category.label,
|
||||
description: `The label for category ${category.label} in sidebar ${sidebarName}`,
|
||||
}))
|
||||
.value();
|
||||
|
||||
const links = collectSidebarLinks(sidebar);
|
||||
const linksContent: TranslationFileContent = chain(links)
|
||||
.keyBy((link) => `sidebar.${sidebarName}.link.${link.label}`)
|
||||
.mapValues((link) => ({
|
||||
message: link.label,
|
||||
description: `The label for link ${link.label} in sidebar ${sidebarName}, linking to ${link.href}`,
|
||||
}))
|
||||
.value();
|
||||
|
||||
return mergeTranslations([categoryContent, linksContent]);
|
||||
}
|
||||
|
||||
function translateSidebar({
|
||||
sidebar,
|
||||
sidebarName,
|
||||
sidebarsTranslations,
|
||||
}: {
|
||||
sidebar: Sidebar;
|
||||
sidebarName: string;
|
||||
sidebarsTranslations: TranslationFileContent;
|
||||
}): Sidebar {
|
||||
return transformSidebarItems(
|
||||
sidebar,
|
||||
(item: SidebarItem): SidebarItem => {
|
||||
if (item.type === 'category') {
|
||||
return {
|
||||
...item,
|
||||
label:
|
||||
sidebarsTranslations[
|
||||
`sidebar.${sidebarName}.category.${item.label}`
|
||||
]?.message ?? item.label,
|
||||
};
|
||||
}
|
||||
if (item.type === 'link') {
|
||||
return {
|
||||
...item,
|
||||
label:
|
||||
sidebarsTranslations[`sidebar.${sidebarName}.link.${item.label}`]
|
||||
?.message ?? item.label,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function getSidebarsTranslations(
|
||||
version: LoadedVersion,
|
||||
): TranslationFileContent {
|
||||
return mergeTranslations(
|
||||
Object.entries(version.sidebars).map(([sidebarName, sidebar]) => {
|
||||
const normalizedSidebarName = getNormalizedSidebarName({
|
||||
sidebarName,
|
||||
versionName: version.versionName,
|
||||
});
|
||||
return getSidebarTranslationFileContent(sidebar, normalizedSidebarName);
|
||||
}),
|
||||
);
|
||||
}
|
||||
function translateSidebars(
|
||||
version: LoadedVersion,
|
||||
sidebarsTranslations: TranslationFileContent,
|
||||
): Sidebars {
|
||||
return mapValues(version.sidebars, (sidebar, sidebarName) => {
|
||||
return translateSidebar({
|
||||
sidebar,
|
||||
sidebarName: getNormalizedSidebarName({
|
||||
sidebarName,
|
||||
versionName: version.versionName,
|
||||
}),
|
||||
sidebarsTranslations,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getVersionTranslationFiles(version: LoadedVersion): TranslationFiles {
|
||||
const versionTranslations: TranslationFileContent = {
|
||||
'version.label': {
|
||||
message: version.versionLabel,
|
||||
description: `The label for version ${version.versionName}`,
|
||||
},
|
||||
};
|
||||
|
||||
const sidebarsTranslations: TranslationFileContent = getSidebarsTranslations(
|
||||
version,
|
||||
);
|
||||
|
||||
// const docsTranslations: TranslationFileContent = getDocsTranslations(version);
|
||||
|
||||
return [
|
||||
{
|
||||
path: getVersionFileName(version.versionName),
|
||||
content: mergeTranslations([
|
||||
versionTranslations,
|
||||
sidebarsTranslations,
|
||||
// docsTranslations,
|
||||
]),
|
||||
},
|
||||
];
|
||||
}
|
||||
function translateVersion(
|
||||
version: LoadedVersion,
|
||||
translationFiles: Record<string, TranslationFile>,
|
||||
): LoadedVersion {
|
||||
const versionTranslations =
|
||||
translationFiles[getVersionFileName(version.versionName)].content;
|
||||
return {
|
||||
...version,
|
||||
versionLabel: versionTranslations['version.label']?.message,
|
||||
sidebars: translateSidebars(version, versionTranslations),
|
||||
// docs: translateDocs(version.docs, versionTranslations),
|
||||
};
|
||||
}
|
||||
|
||||
function getVersionsTranslationFiles(
|
||||
versions: LoadedVersion[],
|
||||
): TranslationFiles {
|
||||
return flatten(versions.map(getVersionTranslationFiles));
|
||||
}
|
||||
function translateVersions(
|
||||
versions: LoadedVersion[],
|
||||
translationFiles: Record<string, TranslationFile>,
|
||||
): LoadedVersion[] {
|
||||
return versions.map((version) => translateVersion(version, translationFiles));
|
||||
}
|
||||
|
||||
export function getLoadedContentTranslationFiles(
|
||||
loadedContent: LoadedContent,
|
||||
): TranslationFiles {
|
||||
return getVersionsTranslationFiles(loadedContent.loadedVersions);
|
||||
}
|
||||
export function translateLoadedContent(
|
||||
loadedContent: LoadedContent,
|
||||
translationFiles: TranslationFile[],
|
||||
): LoadedContent {
|
||||
const translationFilesMap: Record<string, TranslationFile> = keyBy(
|
||||
translationFiles,
|
||||
(f) => f.path,
|
||||
);
|
||||
|
||||
return {
|
||||
loadedVersions: translateVersions(
|
||||
loadedContent.loadedVersions,
|
||||
translationFilesMap,
|
||||
),
|
||||
};
|
||||
}
|
|
@ -9,6 +9,7 @@
|
|||
/// <reference types="@docusaurus/module-type-aliases" />
|
||||
|
||||
export type DocFile = {
|
||||
filePath: string;
|
||||
source: string;
|
||||
content: string;
|
||||
lastUpdate: LastUpdateData;
|
||||
|
@ -21,7 +22,8 @@ export type VersionMetadata = {
|
|||
versionLabel: string; // Version 1.0.0
|
||||
versionPath: string; // /baseUrl/docs/1.0.0
|
||||
isLast: boolean;
|
||||
docsDirPath: string; // versioned_docs/1.0.0
|
||||
docsDirPath: string; // "versioned_docs/version-1.0.0"
|
||||
docsDirPathLocalized: string; // "i18n/fr/version-1.0.0/default"
|
||||
sidebarFilePath: string; // versioned_sidebars/1.0.0.json
|
||||
routePriority: number | undefined; // -1 for the latest docs
|
||||
};
|
||||
|
@ -91,6 +93,7 @@ export type SidebarItem =
|
|||
| SidebarItemCategory;
|
||||
|
||||
export type Sidebar = SidebarItem[];
|
||||
export type SidebarItemType = SidebarItem['type'];
|
||||
|
||||
export type Sidebars = Record<string, Sidebar>;
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
|
||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
|
||||
import {LoadContext} from '@docusaurus/types';
|
||||
import {normalizeUrl} from '@docusaurus/utils';
|
||||
import {getPluginI18nPath, normalizeUrl} from '@docusaurus/utils';
|
||||
import {difference} from 'lodash';
|
||||
import chalk from 'chalk';
|
||||
|
||||
|
@ -137,9 +137,12 @@ function getVersionMetadataPaths({
|
|||
options,
|
||||
}: {
|
||||
versionName: string;
|
||||
context: Pick<LoadContext, 'siteDir'>;
|
||||
context: Pick<LoadContext, 'siteDir' | 'i18n'>;
|
||||
options: Pick<PluginOptions, 'id' | 'path' | 'sidebarPath'>;
|
||||
}): Pick<VersionMetadata, 'docsDirPath' | 'sidebarFilePath'> {
|
||||
}): Pick<
|
||||
VersionMetadata,
|
||||
'docsDirPath' | 'docsDirPathLocalized' | 'sidebarFilePath'
|
||||
> {
|
||||
const isCurrentVersion = versionName === CURRENT_VERSION_NAME;
|
||||
|
||||
const docsDirPath = isCurrentVersion
|
||||
|
@ -149,6 +152,18 @@ function getVersionMetadataPaths({
|
|||
`version-${versionName}`,
|
||||
);
|
||||
|
||||
const docsDirPathLocalized = getPluginI18nPath({
|
||||
siteDir: context.siteDir,
|
||||
locale: context.i18n.currentLocale,
|
||||
pluginName: 'docusaurus-plugin-content-docs',
|
||||
pluginId: options.id,
|
||||
subPaths: [
|
||||
versionName === CURRENT_VERSION_NAME
|
||||
? CURRENT_VERSION_NAME
|
||||
: `version-${versionName}`,
|
||||
],
|
||||
});
|
||||
|
||||
const sidebarFilePath = isCurrentVersion
|
||||
? path.resolve(context.siteDir, options.sidebarPath)
|
||||
: path.join(
|
||||
|
@ -156,7 +171,7 @@ function getVersionMetadataPaths({
|
|||
`version-${versionName}-sidebars.json`,
|
||||
);
|
||||
|
||||
return {docsDirPath, sidebarFilePath};
|
||||
return {docsDirPath, docsDirPathLocalized, sidebarFilePath};
|
||||
}
|
||||
|
||||
function createVersionMetadata({
|
||||
|
@ -167,13 +182,17 @@ function createVersionMetadata({
|
|||
}: {
|
||||
versionName: string;
|
||||
isLast: boolean;
|
||||
context: Pick<LoadContext, 'siteDir' | 'baseUrl'>;
|
||||
context: Pick<LoadContext, 'siteDir' | 'baseUrl' | 'i18n'>;
|
||||
options: Pick<
|
||||
PluginOptions,
|
||||
'id' | 'path' | 'sidebarPath' | 'routeBasePath' | 'versions'
|
||||
>;
|
||||
}): VersionMetadata {
|
||||
const {sidebarFilePath, docsDirPath} = getVersionMetadataPaths({
|
||||
const {
|
||||
sidebarFilePath,
|
||||
docsDirPath,
|
||||
docsDirPathLocalized,
|
||||
} = getVersionMetadataPaths({
|
||||
versionName,
|
||||
context,
|
||||
options,
|
||||
|
@ -210,6 +229,7 @@ function createVersionMetadata({
|
|||
routePriority,
|
||||
sidebarFilePath,
|
||||
docsDirPath,
|
||||
docsDirPathLocalized,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -322,7 +342,7 @@ export function readVersionsMetadata({
|
|||
context,
|
||||
options,
|
||||
}: {
|
||||
context: Pick<LoadContext, 'siteDir' | 'baseUrl'>;
|
||||
context: Pick<LoadContext, 'siteDir' | 'baseUrl' | 'i18n'>;
|
||||
options: Pick<
|
||||
PluginOptions,
|
||||
| 'id'
|
||||
|
@ -356,3 +376,15 @@ export function readVersionsMetadata({
|
|||
versionsMetadata.forEach(checkVersionMetadataPaths);
|
||||
return versionsMetadata;
|
||||
}
|
||||
|
||||
// order matter!
|
||||
// Read in priority the localized path, then the unlocalized one
|
||||
// We want the localized doc to "override" the unlocalized one
|
||||
export function getDocsDirPaths(
|
||||
versionMetadata: Pick<
|
||||
VersionMetadata,
|
||||
'docsDirPath' | 'docsDirPathLocalized'
|
||||
>,
|
||||
): [string, string] {
|
||||
return [versionMetadata.docsDirPathLocalized, versionMetadata.docsDirPath];
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
"globby": "^10.0.1",
|
||||
"joi": "^17.2.1",
|
||||
"loader-utils": "^1.2.3",
|
||||
"lodash": "^4.17.19",
|
||||
"minimatch": "^3.0.4",
|
||||
"remark-admonitions": "^1.2.1",
|
||||
"slash": "^3.0.0",
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* 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';
|
||||
|
||||
export default class TranslatedJs extends React.Component {
|
||||
render() {
|
||||
return <div>TranslatedJsPage (fr)</div>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
translated markdown page (fr)
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* 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';
|
||||
|
||||
export default class TranslatedJs extends React.Component {
|
||||
render() {
|
||||
return <div>TranslatedJsPage</div>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
translated markdown page
|
|
@ -14,7 +14,7 @@ import normalizePluginOptions from './pluginOptionSchema.test';
|
|||
describe('docusaurus-plugin-content-pages', () => {
|
||||
test('simple pages', async () => {
|
||||
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
||||
const context = loadContext(siteDir);
|
||||
const context = await loadContext(siteDir);
|
||||
const pluginPath = 'src/pages';
|
||||
const plugin = pluginContentPages(
|
||||
context,
|
||||
|
@ -22,7 +22,7 @@ describe('docusaurus-plugin-content-pages', () => {
|
|||
path: pluginPath,
|
||||
}),
|
||||
);
|
||||
const pagesMetadatas = await plugin.loadContent();
|
||||
const pagesMetadatas = (await plugin.loadContent?.())!;
|
||||
|
||||
expect(pagesMetadatas).toEqual([
|
||||
{
|
||||
|
@ -45,6 +45,80 @@ describe('docusaurus-plugin-content-pages', () => {
|
|||
permalink: '/hello/mdxPage',
|
||||
source: path.join('@site', pluginPath, 'hello', 'mdxPage.mdx'),
|
||||
},
|
||||
{
|
||||
type: 'jsx',
|
||||
permalink: '/hello/translatedJs',
|
||||
source: path.join('@site', pluginPath, 'hello', 'translatedJs.js'),
|
||||
},
|
||||
{
|
||||
type: 'mdx',
|
||||
permalink: '/hello/translatedMd',
|
||||
source: path.join('@site', pluginPath, 'hello', 'translatedMd.md'),
|
||||
},
|
||||
{
|
||||
type: 'jsx',
|
||||
permalink: '/hello/world',
|
||||
source: path.join('@site', pluginPath, 'hello', 'world.js'),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('simple pages with french translations', async () => {
|
||||
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
||||
const context = await loadContext(siteDir);
|
||||
const pluginPath = 'src/pages';
|
||||
const plugin = pluginContentPages(
|
||||
{
|
||||
...context,
|
||||
i18n: {
|
||||
...context.i18n,
|
||||
currentLocale: 'fr',
|
||||
},
|
||||
},
|
||||
normalizePluginOptions({
|
||||
path: pluginPath,
|
||||
}),
|
||||
);
|
||||
const pagesMetadatas = (await plugin.loadContent?.())!;
|
||||
|
||||
const frTranslationsPath = path.join(
|
||||
'@site',
|
||||
'i18n',
|
||||
'fr',
|
||||
'docusaurus-plugin-content-pages',
|
||||
);
|
||||
|
||||
expect(pagesMetadatas).toEqual([
|
||||
{
|
||||
type: 'jsx',
|
||||
permalink: '/',
|
||||
source: path.join('@site', pluginPath, 'index.js'),
|
||||
},
|
||||
{
|
||||
type: 'jsx',
|
||||
permalink: '/typescript',
|
||||
source: path.join('@site', pluginPath, 'typescript.tsx'),
|
||||
},
|
||||
{
|
||||
type: 'mdx',
|
||||
permalink: '/hello/',
|
||||
source: path.join('@site', pluginPath, 'hello', 'index.md'),
|
||||
},
|
||||
{
|
||||
type: 'mdx',
|
||||
permalink: '/hello/mdxPage',
|
||||
source: path.join('@site', pluginPath, 'hello', 'mdxPage.mdx'),
|
||||
},
|
||||
{
|
||||
type: 'jsx',
|
||||
permalink: '/hello/translatedJs',
|
||||
source: path.join(frTranslationsPath, 'hello', 'translatedJs.js'),
|
||||
},
|
||||
{
|
||||
type: 'mdx',
|
||||
permalink: '/hello/translatedMd',
|
||||
source: path.join(frTranslationsPath, 'hello', 'translatedMd.md'),
|
||||
},
|
||||
{
|
||||
type: 'jsx',
|
||||
permalink: '/hello/world',
|
||||
|
|
|
@ -15,6 +15,8 @@ import {
|
|||
fileToPath,
|
||||
aliasedSitePath,
|
||||
docuHash,
|
||||
getPluginI18nPath,
|
||||
getFolderContainingFile,
|
||||
} from '@docusaurus/utils';
|
||||
import {
|
||||
LoadContext,
|
||||
|
@ -32,7 +34,17 @@ import {
|
|||
STATIC_DIR_NAME,
|
||||
} from '@docusaurus/core/lib/constants';
|
||||
|
||||
import {PluginOptions, LoadedContent, Metadata} from './types';
|
||||
import {
|
||||
PluginOptions,
|
||||
LoadedContent,
|
||||
Metadata,
|
||||
PagesContentPaths,
|
||||
} from './types';
|
||||
import {flatten} from 'lodash';
|
||||
|
||||
export function getContentPathList(contentPaths: PagesContentPaths) {
|
||||
return [contentPaths.contentPathLocalized, contentPaths.contentPath];
|
||||
}
|
||||
|
||||
const isMarkdownSource = (source: string) =>
|
||||
source.endsWith('.md') || source.endsWith('.mdx');
|
||||
|
@ -46,9 +58,22 @@ export default function pluginContentPages(
|
|||
[admonitions, options.admonitions || {}],
|
||||
]);
|
||||
}
|
||||
const {siteConfig, siteDir, generatedFilesDir} = context;
|
||||
const {
|
||||
siteConfig,
|
||||
siteDir,
|
||||
generatedFilesDir,
|
||||
i18n: {currentLocale},
|
||||
} = context;
|
||||
|
||||
const contentPath = path.resolve(siteDir, options.path);
|
||||
const contentPaths: PagesContentPaths = {
|
||||
contentPath: path.resolve(siteDir, options.path),
|
||||
contentPathLocalized: getPluginI18nPath({
|
||||
siteDir,
|
||||
locale: currentLocale,
|
||||
pluginName: 'docusaurus-plugin-content-pages',
|
||||
pluginId: options.id,
|
||||
}),
|
||||
};
|
||||
|
||||
const pluginDataDirRoot = path.join(
|
||||
generatedFilesDir,
|
||||
|
@ -66,8 +91,11 @@ export default function pluginContentPages(
|
|||
|
||||
getPathsToWatch() {
|
||||
const {include = []} = options;
|
||||
const globPattern = include.map((pattern) => `${contentPath}/${pattern}`);
|
||||
return [...globPattern];
|
||||
return flatten(
|
||||
getContentPathList(contentPaths).map((contentPath) => {
|
||||
return include.map((pattern) => `${contentPath}/${pattern}`);
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
getClientModules() {
|
||||
|
@ -82,20 +110,25 @@ export default function pluginContentPages(
|
|||
|
||||
async loadContent() {
|
||||
const {include} = options;
|
||||
const pagesDir = contentPath;
|
||||
|
||||
if (!fs.existsSync(pagesDir)) {
|
||||
if (!fs.existsSync(contentPaths.contentPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {baseUrl} = siteConfig;
|
||||
const pagesFiles = await globby(include, {
|
||||
cwd: pagesDir,
|
||||
cwd: contentPaths.contentPath,
|
||||
ignore: options.exclude,
|
||||
});
|
||||
|
||||
function toMetadata(relativeSource: string): Metadata {
|
||||
const source = path.join(pagesDir, relativeSource);
|
||||
async function toMetadata(relativeSource: string): Promise<Metadata> {
|
||||
// Lookup in localized folder in priority
|
||||
const contentPath = await getFolderContainingFile(
|
||||
getContentPathList(contentPaths),
|
||||
relativeSource,
|
||||
);
|
||||
|
||||
const source = path.join(contentPath, relativeSource);
|
||||
const aliasedSourcePath = aliasedSitePath(source, siteDir);
|
||||
const pathName = encodePath(fileToPath(relativeSource));
|
||||
const permalink = pathName.replace(/^\//, baseUrl || '');
|
||||
|
@ -114,7 +147,7 @@ export default function pluginContentPages(
|
|||
}
|
||||
}
|
||||
|
||||
return pagesFiles.map(toMetadata);
|
||||
return Promise.all(pagesFiles.map(toMetadata));
|
||||
},
|
||||
|
||||
async contentLoaded({content, actions}) {
|
||||
|
@ -177,7 +210,7 @@ export default function pluginContentPages(
|
|||
rules: [
|
||||
{
|
||||
test: /(\.mdx?)$/,
|
||||
include: [contentPath],
|
||||
include: getContentPathList(contentPaths),
|
||||
use: [
|
||||
getCacheLoader(isServer),
|
||||
getBabelLoader(isServer),
|
||||
|
|
|
@ -34,3 +34,8 @@ export type MDXPageMetadata = {
|
|||
export type Metadata = JSXPageMetadata | MDXPageMetadata;
|
||||
|
||||
export type LoadedContent = Metadata[];
|
||||
|
||||
export type PagesContentPaths = {
|
||||
contentPath: string;
|
||||
contentPathLocalized: string;
|
||||
};
|
||||
|
|
|
@ -6,5 +6,30 @@
|
|||
*/
|
||||
|
||||
module.exports = {
|
||||
presets: [['@babel/preset-typescript', {isTSX: true, allExtensions: true}]],
|
||||
env: {
|
||||
// USED FOR NODE/RUNTIME
|
||||
// maybe we should differenciate both cases because
|
||||
// we mostly need to transpile some features so that node does not crash...
|
||||
lib: {
|
||||
presets: [
|
||||
['@babel/preset-typescript', {isTSX: true, allExtensions: true}],
|
||||
],
|
||||
// Useful to transpile for older node versions
|
||||
plugins: [
|
||||
'@babel/plugin-transform-modules-commonjs',
|
||||
'@babel/plugin-proposal-nullish-coalescing-operator',
|
||||
'@babel/plugin-proposal-optional-chaining',
|
||||
],
|
||||
},
|
||||
|
||||
// USED FOR JS SWIZZLE
|
||||
// /lib-next folder is used as source to swizzle JS source code
|
||||
// This JS code is created from TS source code
|
||||
// This source code should look clean/human readable to be usable
|
||||
'lib-next': {
|
||||
presets: [
|
||||
['@babel/preset-typescript', {isTSX: true, allExtensions: true}],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"name": "@docusaurus/theme-classic",
|
||||
"version": "2.0.0-alpha.69",
|
||||
"description": "Classic theme for Docusaurus",
|
||||
"main": "src/index.js",
|
||||
"main": "lib/index.js",
|
||||
"types": "src/types.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
@ -14,9 +14,10 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit && yarn babel && yarn prettier",
|
||||
"watch": "yarn babel --watch",
|
||||
"babel": "babel src -d lib --extensions \".tsx,.ts\" --ignore \"**/*.d.ts\" --copy-files",
|
||||
"build": "tsc --noEmit && yarn babel:lib && yarn babel:lib-next && yarn prettier",
|
||||
"watch": "yarn babel:lib --watch",
|
||||
"babel:lib": "cross-env BABEL_ENV=lib babel src -d lib --extensions \".tsx,.ts\" --ignore \"**/*.d.ts\" --copy-files",
|
||||
"babel:lib-next": "cross-env BABEL_ENV=lib-next babel src -d lib-next --extensions \".tsx,.ts\" --ignore \"**/*.d.ts\" --copy-files",
|
||||
"prettier": "prettier --config ../../.prettierrc --write \"**/*.{js,ts}\""
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -27,6 +28,7 @@
|
|||
"@docusaurus/theme-common": "2.0.0-alpha.69",
|
||||
"@docusaurus/types": "2.0.0-alpha.69",
|
||||
"@docusaurus/utils-validation": "2.0.0-alpha.69",
|
||||
"@docusaurus/utils": "2.0.0-alpha.69",
|
||||
"@mdx-js/mdx": "^1.6.21",
|
||||
"@mdx-js/react": "^1.6.21",
|
||||
"@types/react-toggle": "^4.0.2",
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`getTranslationFiles should return translation files matching snapshot 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"content": Object {
|
||||
"item.label.Dropdown": Object {
|
||||
"description": "Navbar item with label Dropdown",
|
||||
"message": "Dropdown",
|
||||
},
|
||||
"item.label.Dropdown item 1": Object {
|
||||
"description": "Navbar item with label Dropdown item 1",
|
||||
"message": "Dropdown item 1",
|
||||
},
|
||||
"title": Object {
|
||||
"description": "The title in the navbar",
|
||||
"message": "navbar title",
|
||||
},
|
||||
},
|
||||
"path": "navbar",
|
||||
},
|
||||
Object {
|
||||
"content": Object {
|
||||
"copyright": Object {
|
||||
"description": "The footer copyright",
|
||||
"message": "Copyright FB",
|
||||
},
|
||||
"link.item.label.Link 1": Object {
|
||||
"description": "The label of footer link with label=Link 1 linking to https://facebook.com",
|
||||
"message": "Link 1",
|
||||
},
|
||||
"link.item.label.Link 2": Object {
|
||||
"description": "The label of footer link with label=Link 2 linking to https://facebook.com",
|
||||
"message": "Link 2",
|
||||
},
|
||||
"link.item.label.Link 3": Object {
|
||||
"description": "The label of footer link with label=Link 3 linking to https://facebook.com",
|
||||
"message": "Link 3",
|
||||
},
|
||||
"link.title.Footer link column 1": Object {
|
||||
"description": "The title of the footer links column with title=Footer link column 1 in the footer",
|
||||
"message": "Footer link column 1",
|
||||
},
|
||||
"link.title.Footer link column 2": Object {
|
||||
"description": "The title of the footer links column with title=Footer link column 2 in the footer",
|
||||
"message": "Footer link column 2",
|
||||
},
|
||||
},
|
||||
"path": "footer",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`translateThemeConfig should return translated themeConfig matching snapshot 1`] = `
|
||||
Object {
|
||||
"announcementBar": Object {},
|
||||
"colorMode": Object {},
|
||||
"docs": Object {
|
||||
"versionPersistence": "none",
|
||||
},
|
||||
"footer": Object {
|
||||
"copyright": "Copyright FB (translated)",
|
||||
"links": Array [
|
||||
Object {
|
||||
"items": Array [
|
||||
Object {
|
||||
"label": "Link 1 (translated)",
|
||||
"to": "https://facebook.com",
|
||||
},
|
||||
Object {
|
||||
"label": "Link 2 (translated)",
|
||||
"to": "https://facebook.com",
|
||||
},
|
||||
],
|
||||
"title": "Footer link column 1 (translated)",
|
||||
},
|
||||
Object {
|
||||
"items": Array [
|
||||
Object {
|
||||
"label": "Link 3 (translated)",
|
||||
"to": "https://facebook.com",
|
||||
},
|
||||
],
|
||||
"title": "Footer link column 2 (translated)",
|
||||
},
|
||||
],
|
||||
"style": "light",
|
||||
},
|
||||
"hideableSidebar": true,
|
||||
"navbar": Object {
|
||||
"hideOnScroll": false,
|
||||
"items": Array [
|
||||
Object {
|
||||
"items": Array [
|
||||
Object {
|
||||
"items": Array [],
|
||||
"label": "Dropdown item 1",
|
||||
},
|
||||
],
|
||||
"label": "Dropdown (translated)",
|
||||
},
|
||||
],
|
||||
"style": "dark",
|
||||
"title": "navbar title (translated)",
|
||||
},
|
||||
"prism": Object {},
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,89 @@
|
|||
/**
|
||||
* 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 {getTranslationFiles, translateThemeConfig} from '../translations';
|
||||
import {ThemeConfig} from '@docusaurus/theme-common';
|
||||
import {updateTranslationFileMessages} from '@docusaurus/utils';
|
||||
|
||||
const ThemeConfigSample: ThemeConfig = {
|
||||
colorMode: {},
|
||||
announcementBar: {},
|
||||
prism: {},
|
||||
docs: {
|
||||
versionPersistence: 'none',
|
||||
},
|
||||
hideableSidebar: true,
|
||||
navbar: {
|
||||
title: 'navbar title',
|
||||
style: 'dark',
|
||||
hideOnScroll: false,
|
||||
items: [
|
||||
{label: 'Dropdown', items: [{label: 'Dropdown item 1', items: []}]},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
copyright: 'Copyright FB',
|
||||
style: 'light',
|
||||
links: [
|
||||
{
|
||||
title: 'Footer link column 1',
|
||||
items: [
|
||||
{label: 'Link 1', to: 'https://facebook.com'},
|
||||
{label: 'Link 2', to: 'https://facebook.com'},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Footer link column 2',
|
||||
items: [{label: 'Link 3', to: 'https://facebook.com'}],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
function getSampleTranslationFiles() {
|
||||
return getTranslationFiles({
|
||||
themeConfig: ThemeConfigSample,
|
||||
});
|
||||
}
|
||||
|
||||
function getSampleTranslationFilesTranslated() {
|
||||
const translationFiles = getSampleTranslationFiles();
|
||||
return translationFiles.map((translationFile) =>
|
||||
updateTranslationFileMessages(
|
||||
translationFile,
|
||||
(message) => `${message} (translated)`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
describe('getTranslationFiles', () => {
|
||||
test('should return translation files matching snapshot', () => {
|
||||
expect(getSampleTranslationFiles()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('translateThemeConfig', () => {
|
||||
test('should not translate anything if translation files are untranslated', () => {
|
||||
const translationFiles = getSampleTranslationFiles();
|
||||
expect(
|
||||
translateThemeConfig({
|
||||
themeConfig: ThemeConfigSample,
|
||||
translationFiles,
|
||||
}),
|
||||
).toEqual(ThemeConfigSample);
|
||||
});
|
||||
|
||||
test('should return translated themeConfig matching snapshot', () => {
|
||||
const translationFiles = getSampleTranslationFilesTranslated();
|
||||
expect(
|
||||
translateThemeConfig({
|
||||
themeConfig: ThemeConfigSample,
|
||||
translationFiles,
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -5,9 +5,10 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const Module = require('module');
|
||||
const {validateThemeConfig} = require('./validateThemeConfig');
|
||||
import {Plugin} from '@docusaurus/types';
|
||||
import {getTranslationFiles, translateThemeConfig} from './translations';
|
||||
import path from 'path';
|
||||
import Module from 'module';
|
||||
|
||||
const createRequire = Module.createRequire || Module.createRequireFromPath;
|
||||
const requireFromDocusaurusCore = createRequire(
|
||||
|
@ -58,7 +59,10 @@ const noFlashColorMode = ({defaultMode, respectPrefersColorScheme}) => {
|
|||
})();`;
|
||||
};
|
||||
|
||||
module.exports = function (context, options) {
|
||||
export default function docusaurusThemeClassic(
|
||||
context,
|
||||
options,
|
||||
): Plugin<null, unknown> {
|
||||
const {
|
||||
siteConfig: {themeConfig},
|
||||
} = context;
|
||||
|
@ -69,13 +73,16 @@ module.exports = function (context, options) {
|
|||
name: 'docusaurus-theme-classic',
|
||||
|
||||
getThemePath() {
|
||||
return path.join(__dirname, '..', 'lib', 'theme');
|
||||
return path.join(__dirname, '..', 'lib-next', 'theme');
|
||||
},
|
||||
|
||||
getTypeScriptThemePath() {
|
||||
return path.resolve(__dirname, './theme');
|
||||
},
|
||||
|
||||
getTranslationFiles: async () => getTranslationFiles({themeConfig}),
|
||||
translateThemeConfig,
|
||||
|
||||
getClientModules() {
|
||||
const modules = [
|
||||
require.resolve('infima/dist/css/default/default.css'),
|
||||
|
@ -98,12 +105,15 @@ module.exports = function (context, options) {
|
|||
.map((lang) => `prism-${lang}`)
|
||||
.join('|');
|
||||
|
||||
// See https://github.com/facebook/docusaurus/pull/3382
|
||||
const useDocsWarningFilter = (warning: string) =>
|
||||
warning.includes("Can't resolve '@theme-init/hooks/useDocs");
|
||||
|
||||
return {
|
||||
stats: {
|
||||
warningsFilter: [
|
||||
// See https://github.com/facebook/docusaurus/pull/3382
|
||||
(warning) =>
|
||||
warning.includes("Can't resolve '@theme-init/hooks/useDocs"),
|
||||
// The TS def does not allow function for array item :(
|
||||
useDocsWarningFilter as any,
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
|
@ -129,7 +139,7 @@ module.exports = function (context, options) {
|
|||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const swizzleAllowedComponents = [
|
||||
'CodeBlock',
|
||||
|
@ -141,6 +151,8 @@ const swizzleAllowedComponents = [
|
|||
'prism-include-languages',
|
||||
];
|
||||
|
||||
module.exports.getSwizzleComponentList = () => swizzleAllowedComponents;
|
||||
export function getSwizzleComponentList() {
|
||||
return swizzleAllowedComponents;
|
||||
}
|
||||
|
||||
module.exports.validateThemeConfig = validateThemeConfig;
|
||||
export {validateThemeConfig} from './validateThemeConfig';
|
|
@ -13,7 +13,7 @@ import {useThemeConfig} from '@docusaurus/theme-common';
|
|||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
function FooterLink({to, href, label, prependBaseUrlToHref, ...props}) {
|
||||
function FooterLink({to, href, label, prependBaseUrlToHref, ...props}: any) {
|
||||
const toUrl = useBaseUrl(to);
|
||||
const normalizedHref = useBaseUrl(href, {forcePrependBaseUrl: true});
|
||||
|
||||
|
@ -111,7 +111,7 @@ function Footer(): JSX.Element | null {
|
|||
// Developer provided the HTML, so assume it's safe.
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: copyright,
|
||||
__html: copyright ?? '',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,10 @@ import SearchMetadatas from '@theme/SearchMetadatas';
|
|||
import {DEFAULT_SEARCH_TAG} from '@docusaurus/theme-common';
|
||||
|
||||
export default function LayoutHead(props: Props): JSX.Element {
|
||||
const {siteConfig} = useDocusaurusContext();
|
||||
const {
|
||||
siteConfig,
|
||||
i18n: {currentLocale},
|
||||
} = useDocusaurusContext();
|
||||
const {
|
||||
favicon,
|
||||
title: siteTitle,
|
||||
|
@ -36,11 +39,12 @@ export default function LayoutHead(props: Props): JSX.Element {
|
|||
const metaImage = image || defaultImage;
|
||||
const metaImageUrl = useBaseUrl(metaImage, {absolute: true});
|
||||
const faviconUrl = useBaseUrl(favicon);
|
||||
|
||||
const htmlLang = currentLocale.split('-')[0];
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
{/* TODO: Do not assume that it is in english language */}
|
||||
<html lang="en" />
|
||||
<html lang={htmlLang} />
|
||||
{metaTitle && <title>{metaTitle}</title>}
|
||||
{metaTitle && <meta property="og:title" content={metaTitle} />}
|
||||
{favicon && <link rel="shortcut icon" href={faviconUrl} />}
|
||||
|
@ -63,7 +67,7 @@ export default function LayoutHead(props: Props): JSX.Element {
|
|||
|
||||
<SearchMetadatas
|
||||
tag={DEFAULT_SEARCH_TAG}
|
||||
language="en" // TODO i18n
|
||||
locale={currentLocale}
|
||||
{...searchMetadatas}
|
||||
/>
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import isInternalUrl from '@docusaurus/isInternalUrl';
|
|||
const Logo = (props: Props): JSX.Element => {
|
||||
const {isClient} = useDocusaurusContext();
|
||||
const {
|
||||
navbar: {title, logo = {}},
|
||||
navbar: {title, logo = {src: ''}},
|
||||
} = useThemeConfig();
|
||||
|
||||
const {imageClassName, titleClassName, ...propsRest} = props;
|
||||
|
|
|
@ -10,7 +10,7 @@ import React from 'react';
|
|||
import Head from '@docusaurus/Head';
|
||||
|
||||
type SearchTagMetaProps = {
|
||||
language?: string;
|
||||
locale?: string;
|
||||
version?: string;
|
||||
tag?: string;
|
||||
};
|
||||
|
@ -19,13 +19,13 @@ type SearchTagMetaProps = {
|
|||
// We may want to support other search engine plugins too
|
||||
// Search plugins should swizzle/override this comp to add their behavior
|
||||
export default function SearchMetadatas({
|
||||
language,
|
||||
locale,
|
||||
version,
|
||||
tag,
|
||||
}: SearchTagMetaProps) {
|
||||
return (
|
||||
<Head>
|
||||
{language && <meta name="docusaurus_language" content={`${language}`} />}
|
||||
{locale && <meta name="docusaurus_locale" content={`${locale}`} />}
|
||||
{version && <meta name="docusaurus_version" content={version} />}
|
||||
{tag && <meta name="docusaurus_tag" content={tag} />}
|
||||
</Head>
|
||||
|
|
|
@ -10,15 +10,17 @@ import {
|
|||
DEFAULT_SEARCH_TAG,
|
||||
docVersionSearchTag,
|
||||
} from '@docusaurus/theme-common';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
|
||||
type ContextualSearchFilters = {
|
||||
language: string;
|
||||
locale: string;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
// We may want to support multiple search engines, don't couple that to Algolia/DocSearch
|
||||
// Maybe users will want to use its own search engine solution
|
||||
export default function useContextualSearchFilters(): ContextualSearchFilters {
|
||||
const {siteConfig} = useDocusaurusContext();
|
||||
const allDocsData = useAllDocsData();
|
||||
const activePluginAndVersion = useActivePluginAndVersion();
|
||||
const docsPreferredVersionByPluginId = useDocsPreferredVersionByPluginId();
|
||||
|
@ -38,15 +40,13 @@ export default function useContextualSearchFilters(): ContextualSearchFilters {
|
|||
return docVersionSearchTag(pluginId, version.name);
|
||||
}
|
||||
|
||||
const language = 'en'; // TODO i18n
|
||||
|
||||
const tags = [
|
||||
DEFAULT_SEARCH_TAG,
|
||||
...Object.keys(allDocsData).map(getDocPluginTags),
|
||||
];
|
||||
|
||||
return {
|
||||
language,
|
||||
locale: siteConfig.i18n.currentLocale,
|
||||
tags,
|
||||
};
|
||||
}
|
||||
|
|
161
packages/docusaurus-theme-classic/src/translations.ts
Normal file
161
packages/docusaurus-theme-classic/src/translations.ts
Normal file
|
@ -0,0 +1,161 @@
|
|||
/**
|
||||
* 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 {TranslationFile, TranslationFileContent} from '@docusaurus/types';
|
||||
import {
|
||||
ThemeConfig,
|
||||
Navbar,
|
||||
NavbarItem,
|
||||
Footer,
|
||||
} from '@docusaurus/theme-common';
|
||||
|
||||
import {keyBy, chain, flatten} from 'lodash';
|
||||
import {mergeTranslations} from '@docusaurus/utils';
|
||||
|
||||
function getNavbarTranslationFile(navbar: Navbar): TranslationFileContent {
|
||||
// TODO handle properly all the navbar item types here!
|
||||
function flattenNavbarItems(items: NavbarItem[]): NavbarItem[] {
|
||||
const subItems = flatten(
|
||||
items.map((item) => {
|
||||
const allSubItems = flatten([item.items ?? []]);
|
||||
return flattenNavbarItems(allSubItems);
|
||||
}),
|
||||
);
|
||||
return [...items, ...subItems];
|
||||
}
|
||||
|
||||
const allNavbarItems = flattenNavbarItems(navbar.items);
|
||||
|
||||
const navbarItemsTranslations: TranslationFileContent = chain(
|
||||
allNavbarItems.filter((navbarItem) => !!navbarItem.label),
|
||||
)
|
||||
.keyBy((navbarItem) => `item.label.${navbarItem.label}`)
|
||||
.mapValues((navbarItem) => ({
|
||||
message: navbarItem.label!,
|
||||
description: `Navbar item with label ${navbarItem.label}`,
|
||||
}))
|
||||
.value();
|
||||
|
||||
const titleTranslations: TranslationFileContent = navbar.title
|
||||
? {title: {message: navbar.title, description: 'The title in the navbar'}}
|
||||
: {};
|
||||
|
||||
return mergeTranslations([titleTranslations, navbarItemsTranslations]);
|
||||
}
|
||||
function translateNavbar(
|
||||
navbar: Navbar,
|
||||
navbarTranslations: TranslationFileContent,
|
||||
): Navbar {
|
||||
return {
|
||||
...navbar,
|
||||
title: navbarTranslations.title?.message ?? navbar.title,
|
||||
// TODO handle properly all the navbar item types here!
|
||||
items: navbar.items.map((item) => ({
|
||||
...item,
|
||||
label:
|
||||
navbarTranslations[`item.label.${item.label}`]?.message ?? item.label,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function getFooterTranslationFile(footer: Footer): TranslationFileContent {
|
||||
// TODO POC code
|
||||
const footerLinkTitles: TranslationFileContent = chain(
|
||||
footer.links.filter((link) => !!link.title),
|
||||
)
|
||||
.keyBy((link) => `link.title.${link.title}`)
|
||||
.mapValues((link) => ({
|
||||
message: link.title!,
|
||||
description: `The title of the footer links column with title=${link.title} in the footer`,
|
||||
}))
|
||||
.value();
|
||||
|
||||
const footerLinkLabels: TranslationFileContent = chain(
|
||||
flatten(footer.links.map((link) => link.items)).filter(
|
||||
(link) => !!link.label,
|
||||
),
|
||||
)
|
||||
.keyBy((linkItem) => `link.item.label.${linkItem.label}`)
|
||||
.mapValues((linkItem) => ({
|
||||
message: linkItem.label!,
|
||||
description: `The label of footer link with label=${
|
||||
linkItem.label
|
||||
} linking to ${linkItem.to ?? linkItem.href}`,
|
||||
}))
|
||||
.value();
|
||||
|
||||
const copyright: TranslationFileContent = footer.copyright
|
||||
? {
|
||||
copyright: {
|
||||
message: footer.copyright,
|
||||
description: 'The footer copyright',
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
return mergeTranslations([footerLinkTitles, footerLinkLabels, copyright]);
|
||||
}
|
||||
function translateFooter(
|
||||
footer: Footer,
|
||||
footerTranslations: TranslationFileContent,
|
||||
): Footer {
|
||||
const links = footer.links.map((link) => ({
|
||||
...link,
|
||||
title:
|
||||
footerTranslations[`link.title.${link.title}`]?.message ?? link.title,
|
||||
items: link.items.map((linkItem) => ({
|
||||
...linkItem,
|
||||
label:
|
||||
footerTranslations[`link.item.label.${linkItem.label}`]?.message ??
|
||||
linkItem.label,
|
||||
})),
|
||||
}));
|
||||
|
||||
const copyright = footerTranslations.copyright?.message ?? footer.copyright;
|
||||
|
||||
return {
|
||||
...footer,
|
||||
links,
|
||||
copyright,
|
||||
};
|
||||
}
|
||||
|
||||
export function getTranslationFiles({
|
||||
themeConfig,
|
||||
}: {
|
||||
themeConfig: ThemeConfig;
|
||||
}): TranslationFile[] {
|
||||
return [
|
||||
{path: 'navbar', content: getNavbarTranslationFile(themeConfig.navbar)},
|
||||
{path: 'footer', content: getFooterTranslationFile(themeConfig.footer)},
|
||||
];
|
||||
}
|
||||
|
||||
export function translateThemeConfig({
|
||||
themeConfig,
|
||||
translationFiles,
|
||||
}: {
|
||||
themeConfig: ThemeConfig;
|
||||
translationFiles: TranslationFile[];
|
||||
}): ThemeConfig {
|
||||
const translationFilesMap: Record<string, TranslationFile> = keyBy(
|
||||
translationFiles,
|
||||
(f) => f.path,
|
||||
);
|
||||
|
||||
return {
|
||||
...themeConfig,
|
||||
navbar: translateNavbar(
|
||||
themeConfig.navbar,
|
||||
translationFilesMap.navbar.content,
|
||||
),
|
||||
footer: translateFooter(
|
||||
themeConfig.footer,
|
||||
translationFilesMap.footer.content,
|
||||
),
|
||||
};
|
||||
}
|
|
@ -347,7 +347,7 @@ declare module '@theme/NavbarItem' {
|
|||
import type {Props as DocsVersionNavbarItemProps} from '@theme/NavbarItem/DocsVersionNavbarItem';
|
||||
|
||||
export type Props =
|
||||
| ({readonly type: 'default'} & DefaultNavbarItemProps)
|
||||
| ({readonly type?: 'default' | undefined} & DefaultNavbarItemProps)
|
||||
| ({
|
||||
readonly type: 'docsVersionDropdown';
|
||||
} & DocsVersionDropdownNavbarItemProps)
|
||||
|
|
|
@ -5,8 +5,19 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
export {useThemeConfig, ThemeConfig} from './utils/useThemeConfig';
|
||||
export {
|
||||
useThemeConfig,
|
||||
ThemeConfig,
|
||||
Navbar,
|
||||
NavbarItem,
|
||||
NavbarLogo,
|
||||
Footer,
|
||||
FooterLinks,
|
||||
FooterLinkItem,
|
||||
} from './utils/useThemeConfig';
|
||||
|
||||
export {docVersionSearchTag, DEFAULT_SEARCH_TAG} from './utils/searchUtils';
|
||||
|
||||
export {isDocsPluginEnabled} from './utils/docsUtils';
|
||||
|
||||
export {isSamePath} from './utils/pathUtils';
|
||||
|
|
|
@ -8,6 +8,50 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|||
|
||||
export type DocsVersionPersistence = 'localStorage' | 'none';
|
||||
|
||||
// TODO improve
|
||||
export type NavbarItem = {
|
||||
items?: NavbarItem[];
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export type NavbarLogo = {
|
||||
src: string;
|
||||
srcDark?: string;
|
||||
href?: string;
|
||||
target?: string;
|
||||
alt?: string;
|
||||
};
|
||||
|
||||
// TODO improve
|
||||
export type Navbar = {
|
||||
style: 'dark' | 'primary';
|
||||
hideOnScroll: boolean;
|
||||
title?: string;
|
||||
items: NavbarItem[];
|
||||
logo?: NavbarLogo;
|
||||
};
|
||||
|
||||
export type FooterLinkItem = {
|
||||
label?: string;
|
||||
to?: string;
|
||||
href?: string;
|
||||
html?: string;
|
||||
};
|
||||
export type FooterLinks = {
|
||||
title?: string;
|
||||
items: FooterLinkItem[];
|
||||
};
|
||||
export type Footer = {
|
||||
style: 'light' | 'dark';
|
||||
logo?: {
|
||||
alt?: string;
|
||||
src?: string;
|
||||
href?: string;
|
||||
};
|
||||
copyright?: string;
|
||||
links: FooterLinks[];
|
||||
};
|
||||
|
||||
export type ThemeConfig = {
|
||||
docs: {
|
||||
versionPersistence: DocsVersionPersistence;
|
||||
|
@ -18,11 +62,11 @@ export type ThemeConfig = {
|
|||
// and use it in the Joi validation schema?
|
||||
|
||||
// TODO temporary types
|
||||
navbar: any;
|
||||
navbar: Navbar;
|
||||
colorMode: any;
|
||||
announcementBar: any;
|
||||
prism: any;
|
||||
footer: any;
|
||||
footer: Footer;
|
||||
hideableSidebar: any;
|
||||
};
|
||||
|
||||
|
|
|
@ -10,10 +10,14 @@ import React from 'react';
|
|||
import Head from '@docusaurus/Head';
|
||||
|
||||
// Override default/agnostic SearchMetas to use Algolia-specific metadatas
|
||||
export default function AlgoliaSearchMetadatas({language, version, tag}) {
|
||||
export default function AlgoliaSearchMetadatas({locale, version, tag}) {
|
||||
// Seems safe to consider here the locale is the language,
|
||||
// as the existing docsearch:language filter is afaik a regular string-based filter
|
||||
const language = locale;
|
||||
|
||||
return (
|
||||
<Head>
|
||||
{language && <meta name="docsearch:language" content={`${language}`} />}
|
||||
{language && <meta name="docsearch:language" content={language} />}
|
||||
{version && <meta name="docsearch:version" content={version} />}
|
||||
{tag && <meta name="docsearch:docusaurus_tag" content={tag} />}
|
||||
</Head>
|
||||
|
|
|
@ -9,9 +9,10 @@ import useContextualSearchFilters from '@theme/hooks/useContextualSearchFilters'
|
|||
|
||||
// Translate search-engine agnostic search filters to Algolia search filters
|
||||
export default function useAlgoliaContextualFacetFilters() {
|
||||
const {language, tags} = useContextualSearchFilters();
|
||||
const {locale, tags} = useContextualSearchFilters();
|
||||
|
||||
const languageFilter = `language:${language}`;
|
||||
// seems safe to convert locale->language, see AlgoliaSearchMetadatas comment
|
||||
const languageFilter = `language:${locale}`;
|
||||
|
||||
const tagsFilter = tags.map((tag) => `docusaurus_tag:${tag}`);
|
||||
|
||||
|
|
59
packages/docusaurus-types/src/index.d.ts
vendored
59
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -14,6 +14,10 @@ import {MergeStrategy} from 'webpack-merge';
|
|||
|
||||
export type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'error' | 'throw';
|
||||
|
||||
export type ThemeConfig = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export interface DocusaurusConfig {
|
||||
baseUrl: string;
|
||||
baseUrlIssueBanner: boolean;
|
||||
|
@ -21,6 +25,7 @@ export interface DocusaurusConfig {
|
|||
tagline?: string;
|
||||
title: string;
|
||||
url: string;
|
||||
i18n: I18nConfig;
|
||||
onBrokenLinks: ReportingSeverity;
|
||||
onBrokenMarkdownLinks: ReportingSeverity;
|
||||
onDuplicateRoutes: ReportingSeverity;
|
||||
|
@ -31,9 +36,7 @@ export interface DocusaurusConfig {
|
|||
plugins?: PluginConfig[];
|
||||
themes?: PluginConfig[];
|
||||
presets?: PresetConfig[];
|
||||
themeConfig?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
themeConfig: ThemeConfig;
|
||||
customFields?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
@ -78,10 +81,32 @@ export interface DocusaurusSiteMetadata {
|
|||
readonly pluginVersions: Record<string, DocusaurusPluginVersionInformation>;
|
||||
}
|
||||
|
||||
// Inspired by Chrome JSON, because it's a widely supported i18n format
|
||||
// https://developer.chrome.com/apps/i18n-messages
|
||||
// https://support.crowdin.com/file-formats/chrome-json/
|
||||
// https://www.applanga.com/docs/formats/chrome_i18n_json
|
||||
// https://docs.transifex.com/formats/chrome-json
|
||||
// https://help.phrase.com/help/chrome-json-messages
|
||||
export type TranslationMessage = {message: string; description?: string};
|
||||
export type TranslationFileContent = Record<string, TranslationMessage>;
|
||||
export type TranslationFile = {path: string; content: TranslationFileContent};
|
||||
export type TranslationFiles = TranslationFile[];
|
||||
|
||||
export type I18nConfig = {
|
||||
defaultLocale: string;
|
||||
locales: [string, ...string[]];
|
||||
};
|
||||
|
||||
export type I18n = I18nConfig & {
|
||||
currentLocale: string;
|
||||
};
|
||||
|
||||
export interface DocusaurusContext {
|
||||
siteConfig: DocusaurusConfig;
|
||||
siteMetadata: DocusaurusSiteMetadata;
|
||||
globalData: Record<string, any>;
|
||||
i18n: I18n;
|
||||
codeTranslations: Record<string, string>;
|
||||
isClient: boolean;
|
||||
}
|
||||
|
||||
|
@ -104,6 +129,7 @@ export type StartCLIOptions = HostPortCLIOptions & {
|
|||
hotOnly: boolean;
|
||||
open: boolean;
|
||||
poll: boolean | number;
|
||||
locale?: string;
|
||||
};
|
||||
|
||||
export type ServeCLIOptions = HostPortCLIOptions & {
|
||||
|
@ -111,12 +137,16 @@ export type ServeCLIOptions = HostPortCLIOptions & {
|
|||
dir: string;
|
||||
};
|
||||
|
||||
export interface BuildCLIOptions {
|
||||
export type BuildOptions = {
|
||||
bundleAnalyzer: boolean;
|
||||
outDir: string;
|
||||
minify: boolean;
|
||||
skipBuild: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export type BuildCLIOptions = BuildOptions & {
|
||||
locale?: string;
|
||||
};
|
||||
|
||||
export interface LoadContext {
|
||||
siteDir: string;
|
||||
|
@ -124,7 +154,9 @@ export interface LoadContext {
|
|||
siteConfig: DocusaurusConfig;
|
||||
outDir: string;
|
||||
baseUrl: string;
|
||||
i18n: I18n;
|
||||
ssrTemplate?: string;
|
||||
codeTranslations: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface InjectedHtmlTags {
|
||||
|
@ -187,6 +219,23 @@ export interface Plugin<T, U = unknown> {
|
|||
postBodyTags?: HtmlTags;
|
||||
};
|
||||
getSwizzleComponentList?(): string[];
|
||||
|
||||
// translations
|
||||
getTranslationFiles?(): Promise<TranslationFiles>;
|
||||
translateContent?({
|
||||
content,
|
||||
translationFiles,
|
||||
}: {
|
||||
content: T; // the content loaded by this plugin instance
|
||||
translationFiles: TranslationFiles;
|
||||
}): T;
|
||||
translateThemeConfig?({
|
||||
themeConfig,
|
||||
translationFiles,
|
||||
}: {
|
||||
themeConfig: ThemeConfig;
|
||||
translationFiles: TranslationFiles;
|
||||
}): ThemeConfig;
|
||||
}
|
||||
|
||||
export type ConfigureWebpackFn = Plugin<unknown>['configureWebpack'];
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
"escape-string-regexp": "^2.0.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"gray-matter": "^4.0.2",
|
||||
"lodash": "^4.17.20",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.kebabcase": "^4.1.1",
|
||||
"resolve-pathname": "^3.0.0"
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`getFolderContainingFile throw if no folder contain such file 1`] = `
|
||||
"relativeFilePath=[index.test.ts] does not exist in any of these folders:
|
||||
- /abcdef
|
||||
- /gehij
|
||||
- /klmn]"
|
||||
`;
|
|
@ -27,7 +27,14 @@ import {
|
|||
getFilePathForRoutePath,
|
||||
addLeadingSlash,
|
||||
getElementsAround,
|
||||
mergeTranslations,
|
||||
mapAsyncSequencial,
|
||||
findAsyncSequential,
|
||||
findFolderContainingFile,
|
||||
getFolderContainingFile,
|
||||
updateTranslationFileMessages,
|
||||
} from '../index';
|
||||
import {sum} from 'lodash';
|
||||
|
||||
describe('load utils', () => {
|
||||
test('aliasedSitePath', () => {
|
||||
|
@ -560,3 +567,151 @@ describe('getElementsAround', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeTranslations', () => {
|
||||
test('should merge translations', () => {
|
||||
expect(
|
||||
mergeTranslations([
|
||||
{
|
||||
T1: {message: 'T1 message', description: 'T1 desc'},
|
||||
T2: {message: 'T2 message', description: 'T2 desc'},
|
||||
T3: {message: 'T3 message', description: 'T3 desc'},
|
||||
},
|
||||
{
|
||||
T4: {message: 'T4 message', description: 'T4 desc'},
|
||||
},
|
||||
{T2: {message: 'T2 message 2', description: 'T2 desc 2'}},
|
||||
]),
|
||||
).toEqual({
|
||||
T1: {message: 'T1 message', description: 'T1 desc'},
|
||||
T2: {message: 'T2 message 2', description: 'T2 desc 2'},
|
||||
T3: {message: 'T3 message', description: 'T3 desc'},
|
||||
T4: {message: 'T4 message', description: 'T4 desc'},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapAsyncSequencial', () => {
|
||||
function sleep(timeout: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, timeout));
|
||||
}
|
||||
|
||||
test('map sequentially', async () => {
|
||||
const itemToTimeout: Record<string, number> = {
|
||||
'1': 50,
|
||||
'2': 150,
|
||||
'3': 100,
|
||||
};
|
||||
const items = Object.keys(itemToTimeout);
|
||||
|
||||
const itemMapStartsAt: Record<string, number> = {};
|
||||
const itemMapEndsAt: Record<string, number> = {};
|
||||
|
||||
const timeBefore = Date.now();
|
||||
await expect(
|
||||
mapAsyncSequencial(items, async (item) => {
|
||||
const itemTimeout = itemToTimeout[item];
|
||||
itemMapStartsAt[item] = Date.now();
|
||||
await sleep(itemTimeout);
|
||||
itemMapEndsAt[item] = Date.now();
|
||||
return `${item} mapped`;
|
||||
}),
|
||||
).resolves.toEqual(['1 mapped', '2 mapped', '3 mapped']);
|
||||
const timeAfter = Date.now();
|
||||
|
||||
const timeTotal = timeAfter - timeBefore;
|
||||
|
||||
const totalTimeouts = sum(Object.values(itemToTimeout));
|
||||
expect(timeTotal > totalTimeouts);
|
||||
|
||||
expect(itemMapStartsAt['1'] > 0);
|
||||
expect(itemMapStartsAt['2'] > itemMapEndsAt['1']);
|
||||
expect(itemMapStartsAt['3'] > itemMapEndsAt['2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAsyncSequencial', () => {
|
||||
function sleep(timeout: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, timeout));
|
||||
}
|
||||
|
||||
test('find sequentially', async () => {
|
||||
const items = ['1', '2', '3'];
|
||||
|
||||
const findFn = jest.fn(async (item: string) => {
|
||||
await sleep(50);
|
||||
return item === '2';
|
||||
});
|
||||
|
||||
const timeBefore = Date.now();
|
||||
await expect(findAsyncSequential(items, findFn)).resolves.toEqual('2');
|
||||
const timeAfter = Date.now();
|
||||
|
||||
expect(findFn).toHaveBeenCalledTimes(2);
|
||||
expect(findFn).toHaveBeenNthCalledWith(1, '1');
|
||||
expect(findFn).toHaveBeenNthCalledWith(2, '2');
|
||||
|
||||
const timeTotal = timeAfter - timeBefore;
|
||||
expect(timeTotal > 100);
|
||||
expect(timeTotal < 150);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findFolderContainingFile', () => {
|
||||
test('find appropriate folder', async () => {
|
||||
await expect(
|
||||
findFolderContainingFile(
|
||||
['/abcdef', '/gehij', __dirname, '/klmn'],
|
||||
'index.test.ts',
|
||||
),
|
||||
).resolves.toEqual(__dirname);
|
||||
});
|
||||
|
||||
test('return undefined if no folder contain such file', async () => {
|
||||
await expect(
|
||||
findFolderContainingFile(['/abcdef', '/gehij', '/klmn'], 'index.test.ts'),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFolderContainingFile', () => {
|
||||
test('get appropriate folder', async () => {
|
||||
await expect(
|
||||
getFolderContainingFile(
|
||||
['/abcdef', '/gehij', __dirname, '/klmn'],
|
||||
'index.test.ts',
|
||||
),
|
||||
).resolves.toEqual(__dirname);
|
||||
});
|
||||
|
||||
test('throw if no folder contain such file', async () => {
|
||||
await expect(
|
||||
getFolderContainingFile(['/abcdef', '/gehij', '/klmn'], 'index.test.ts'),
|
||||
).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTranslationFileMessages', () => {
|
||||
test('should update messages', () => {
|
||||
expect(
|
||||
updateTranslationFileMessages(
|
||||
{
|
||||
path: 'abc',
|
||||
content: {
|
||||
t1: {message: 't1 message', description: 't1 desc'},
|
||||
t2: {message: 't2 message', description: 't2 desc'},
|
||||
t3: {message: 't3 message', description: 't3 desc'},
|
||||
},
|
||||
},
|
||||
(message) => `prefix ${message} suffix`,
|
||||
),
|
||||
).toEqual({
|
||||
path: 'abc',
|
||||
content: {
|
||||
t1: {message: 'prefix t1 message suffix', description: 't1 desc'},
|
||||
t2: {message: 'prefix t2 message suffix', description: 't2 desc'},
|
||||
t3: {message: 'prefix t3 message suffix', description: 't3 desc'},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,10 +14,15 @@ import kebabCase from 'lodash.kebabcase';
|
|||
import escapeStringRegexp from 'escape-string-regexp';
|
||||
import fs from 'fs-extra';
|
||||
import {URL} from 'url';
|
||||
import {ReportingSeverity} from '@docusaurus/types';
|
||||
import {
|
||||
ReportingSeverity,
|
||||
TranslationFileContent,
|
||||
TranslationFile,
|
||||
} from '@docusaurus/types';
|
||||
|
||||
// @ts-expect-error: no typedefs :s
|
||||
import resolvePathnameUnsafe from 'resolve-pathname';
|
||||
import {mapValues} from 'lodash';
|
||||
|
||||
const fileHash = new Map();
|
||||
export async function generate(
|
||||
|
@ -439,6 +444,89 @@ export function getElementsAround<T extends unknown>(
|
|||
return {previous, next};
|
||||
}
|
||||
|
||||
export function getPluginI18nPath({
|
||||
siteDir,
|
||||
locale,
|
||||
pluginName,
|
||||
pluginId = 'default', // TODO duplicated constant
|
||||
subPaths = [],
|
||||
}: {
|
||||
siteDir: string;
|
||||
locale: string;
|
||||
pluginName: string;
|
||||
pluginId?: string | undefined;
|
||||
subPaths?: string[];
|
||||
}) {
|
||||
return path.join(
|
||||
siteDir,
|
||||
'i18n',
|
||||
// namespace first by locale: convenient to work in a single folder for a translator
|
||||
locale,
|
||||
// Make it convenient to use for single-instance
|
||||
// ie: return "docs", not "docs-default" nor "docs/default"
|
||||
`${pluginName}${
|
||||
// TODO duplicate constant :(
|
||||
pluginId === 'default' ? '' : `-${pluginId}`
|
||||
}`,
|
||||
...subPaths,
|
||||
);
|
||||
}
|
||||
|
||||
export async function mapAsyncSequencial<T extends unknown, R extends unknown>(
|
||||
array: T[],
|
||||
action: (t: T) => Promise<R>,
|
||||
): Promise<R[]> {
|
||||
const results: R[] = [];
|
||||
for (const t of array) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const result = await action(t);
|
||||
results.push(result);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function findAsyncSequential<T>(
|
||||
array: T[],
|
||||
predicate: (t: T) => Promise<boolean>,
|
||||
): Promise<T | undefined> {
|
||||
for (const t of array) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
if (await predicate(t)) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// return the first folder path in which the file exists in
|
||||
export async function findFolderContainingFile(
|
||||
folderPaths: string[],
|
||||
relativeFilePath: string,
|
||||
): Promise<string | undefined> {
|
||||
return findAsyncSequential(folderPaths, (folderPath) =>
|
||||
fs.pathExists(path.join(folderPath, relativeFilePath)),
|
||||
);
|
||||
}
|
||||
|
||||
export async function getFolderContainingFile(
|
||||
folderPaths: string[],
|
||||
relativeFilePath: string,
|
||||
): Promise<string> {
|
||||
const maybeFolderPath = await findFolderContainingFile(
|
||||
folderPaths,
|
||||
relativeFilePath,
|
||||
);
|
||||
// should never happen, as the source was read from the FS anyway...
|
||||
if (!maybeFolderPath) {
|
||||
throw new Error(
|
||||
`relativeFilePath=[${relativeFilePath}] does not exist in any of these folders: \n- ${folderPaths.join(
|
||||
'\n- ',
|
||||
)}]`,
|
||||
);
|
||||
}
|
||||
return maybeFolderPath;
|
||||
}
|
||||
|
||||
export function reportMessage(
|
||||
message: string,
|
||||
reportingSeverity: ReportingSeverity,
|
||||
|
@ -464,6 +552,14 @@ export function reportMessage(
|
|||
}
|
||||
}
|
||||
|
||||
export function mergeTranslations(
|
||||
contents: TranslationFileContent[],
|
||||
): TranslationFileContent {
|
||||
return contents.reduce((acc, content) => {
|
||||
return {...acc, ...content};
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function getSwizzledComponent(
|
||||
componentPath: string,
|
||||
): string | undefined {
|
||||
|
@ -477,3 +573,18 @@ export function getSwizzledComponent(
|
|||
? swizzledComponentPath
|
||||
: undefined;
|
||||
}
|
||||
|
||||
// Useful to update all the messages of a translation file
|
||||
// Used in tests to simulate translations
|
||||
export function updateTranslationFileMessages(
|
||||
translationFile: TranslationFile,
|
||||
updateMessage: (message: string) => string,
|
||||
): TranslationFile {
|
||||
return {
|
||||
...translationFile,
|
||||
content: mapValues(translationFile.content, (translation) => ({
|
||||
...translation,
|
||||
message: updateMessage(translation.message),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ const {
|
|||
externalCommand,
|
||||
serve,
|
||||
clear,
|
||||
writeTranslations,
|
||||
} = require('../lib');
|
||||
const requiredVersion = require('../package.json').engines.node;
|
||||
const pkg = require('../package.json');
|
||||
|
@ -89,14 +90,19 @@ cli
|
|||
'--out-dir <dir>',
|
||||
'The full path for the new output directory, relative to the current workspace (default: build).',
|
||||
)
|
||||
.option(
|
||||
'-l, --locale <locale>',
|
||||
'Build the site in a specified locale. Build all known locales otherwise.',
|
||||
)
|
||||
.option(
|
||||
'--no-minify',
|
||||
'Build website without minimizing JS bundles (default: false)',
|
||||
)
|
||||
.action((siteDir = '.', {bundleAnalyzer, outDir, minify}) => {
|
||||
.action((siteDir = '.', {bundleAnalyzer, outDir, locale, minify}) => {
|
||||
wrapCommand(build)(path.resolve(siteDir), {
|
||||
bundleAnalyzer,
|
||||
outDir,
|
||||
locale,
|
||||
minify,
|
||||
});
|
||||
});
|
||||
|
@ -122,6 +128,10 @@ cli
|
|||
cli
|
||||
.command('deploy [siteDir]')
|
||||
.description('Deploy website to GitHub pages')
|
||||
.option(
|
||||
'-l, --locale <locale>',
|
||||
'Deploy the site in a specified locale. Deploy all known locales otherwise.',
|
||||
)
|
||||
.option(
|
||||
'--out-dir <dir>',
|
||||
'The full path for the new output directory, relative to the current workspace (default: build).',
|
||||
|
@ -139,6 +149,7 @@ cli
|
|||
.description('Start the development server')
|
||||
.option('-p, --port <port>', 'use specified port (default: 3000)')
|
||||
.option('-h, --host <host>', 'use specified host (default: localhost')
|
||||
.option('-l, --locale <locale>', 'use specified site locale')
|
||||
.option(
|
||||
'--hot-only',
|
||||
'Do not fallback to page refresh if hot reload fails (default: false)',
|
||||
|
@ -148,10 +159,11 @@ cli
|
|||
'--poll [interval]',
|
||||
'Use polling rather than watching for reload (default: false). Can specify a poll interval in milliseconds.',
|
||||
)
|
||||
.action((siteDir = '.', {port, host, hotOnly, open, poll}) => {
|
||||
.action((siteDir = '.', {port, host, locale, hotOnly, open, poll}) => {
|
||||
wrapCommand(start)(path.resolve(siteDir), {
|
||||
port,
|
||||
host,
|
||||
locale,
|
||||
hotOnly,
|
||||
open,
|
||||
poll,
|
||||
|
@ -188,12 +200,40 @@ cli
|
|||
);
|
||||
|
||||
cli
|
||||
.command('clear')
|
||||
.command('clear [siteDir]')
|
||||
.description('Remove build artifacts')
|
||||
.action(() => {
|
||||
wrapCommand(clear)(path.resolve('.'));
|
||||
.action((siteDir = '.') => {
|
||||
wrapCommand(clear)(path.resolve(siteDir));
|
||||
});
|
||||
|
||||
cli
|
||||
.command('write-translations [siteDir]')
|
||||
.description('Extract required translations of your site')
|
||||
.option(
|
||||
'-l, --locale <locale>',
|
||||
'The locale folder to write the translations\n"--locale fr" will write translations in ./i18n/fr folder)',
|
||||
)
|
||||
.option(
|
||||
'--override',
|
||||
'By default, we only append missing translation messages to existing translation files. This option allows to override existing translation messages. Make sure to commit or backup your existing translations, as they may be overridden.',
|
||||
)
|
||||
.option(
|
||||
'--messagePrefix <messagePrefix>',
|
||||
'Allows to init new written messages with a given prefix. This might help you to highlight untranslated message to make them stand out in the UI.',
|
||||
)
|
||||
.action(
|
||||
(
|
||||
siteDir = '.',
|
||||
{locale = undefined, override = false, messagePrefix = ''},
|
||||
) => {
|
||||
wrapCommand(writeTranslations)(path.resolve(siteDir), {
|
||||
locale,
|
||||
override,
|
||||
messagePrefix,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cli.arguments('<command>').action((cmd) => {
|
||||
cli.outputHelp();
|
||||
console.log(` ${chalk.red(`\n Unknown command ${chalk.yellow(cmd)}.`)}`);
|
||||
|
@ -201,9 +241,15 @@ cli.arguments('<command>').action((cmd) => {
|
|||
});
|
||||
|
||||
function isInternalCommand(command) {
|
||||
return ['start', 'build', 'swizzle', 'deploy', 'serve', 'clear'].includes(
|
||||
command,
|
||||
);
|
||||
return [
|
||||
'start',
|
||||
'build',
|
||||
'swizzle',
|
||||
'deploy',
|
||||
'serve',
|
||||
'clear',
|
||||
'write-translations',
|
||||
].includes(command);
|
||||
}
|
||||
|
||||
if (!isInternalCommand(process.argv.slice(2)[0])) {
|
||||
|
|
|
@ -32,10 +32,12 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "2.0.0-alpha.69",
|
||||
"@types/detect-port": "^1.3.0"
|
||||
"@types/detect-port": "^1.3.0",
|
||||
"tmp-promise": "^3.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.12.3",
|
||||
"@babel/generator": "^7.12.5",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
|
@ -45,6 +47,7 @@
|
|||
"@babel/preset-typescript": "^7.12.1",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@babel/runtime-corejs3": "^7.12.5",
|
||||
"@babel/traverse": "^7.12.5",
|
||||
"@docusaurus/cssnano-preset": "2.0.0-alpha.69",
|
||||
"@docusaurus/types": "2.0.0-alpha.69",
|
||||
"@docusaurus/utils": "2.0.0-alpha.69",
|
||||
|
|
|
@ -79,4 +79,4 @@ function babelPresets(api: ConfigAPI): TransformOptions {
|
|||
return getTransformOptions(callerName === 'server');
|
||||
}
|
||||
|
||||
export = babelPresets;
|
||||
export default babelPresets;
|
||||
|
|
|
@ -10,6 +10,8 @@ import React, {useEffect, useState} from 'react';
|
|||
import routes from '@generated/routes';
|
||||
import siteConfig from '@generated/docusaurus.config';
|
||||
import globalData from '@generated/globalData';
|
||||
import i18n from '@generated/i18n';
|
||||
import codeTranslations from '@generated/codeTranslations';
|
||||
import siteMetadata from '@generated/site-metadata';
|
||||
import renderRoutes from './exports/renderRoutes';
|
||||
import DocusaurusContext from './exports/context';
|
||||
|
@ -27,7 +29,14 @@ function App(): JSX.Element {
|
|||
|
||||
return (
|
||||
<DocusaurusContext.Provider
|
||||
value={{siteConfig, siteMetadata, globalData, isClient}}>
|
||||
value={{
|
||||
siteConfig,
|
||||
siteMetadata,
|
||||
globalData,
|
||||
i18n,
|
||||
codeTranslations,
|
||||
isClient,
|
||||
}}>
|
||||
<BaseUrlIssueBanner />
|
||||
<PendingNavigation routes={routes}>
|
||||
{renderRoutes(routes)}
|
||||
|
|
48
packages/docusaurus/src/client/exports/Translate.tsx
Normal file
48
packages/docusaurus/src/client/exports/Translate.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* 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';
|
||||
|
||||
// Can't read it from context, due to exposing imperative API
|
||||
import codeTranslations from '@generated/codeTranslations';
|
||||
|
||||
function getLocalizedMessage({
|
||||
id,
|
||||
message,
|
||||
}: {
|
||||
message: string;
|
||||
id?: string;
|
||||
}): string {
|
||||
return codeTranslations[id ?? message] ?? message;
|
||||
}
|
||||
|
||||
export type TranslateParam = {
|
||||
message: string;
|
||||
id?: string;
|
||||
description?: string;
|
||||
};
|
||||
// Imperative translation API is useful for some edge-cases:
|
||||
// - translating page titles (meta)
|
||||
// - translating string props (input placeholders, image alt, aria labels...)
|
||||
export function translate({message, id}: TranslateParam): string {
|
||||
const localizedMessage = getLocalizedMessage({message, id});
|
||||
return localizedMessage ?? message;
|
||||
}
|
||||
|
||||
export type TranslateProps = {
|
||||
children: string;
|
||||
id?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
// Maybe we'll want to improve this component with additional features
|
||||
// Like toggling a translation mode that adds a little translation button near the text?
|
||||
export default function Translate({children, id}: TranslateProps): JSX.Element {
|
||||
const localizedMessage: string =
|
||||
getLocalizedMessage({message: children, id}) ?? children;
|
||||
return <>{localizedMessage}</>;
|
||||
}
|
|
@ -22,17 +22,76 @@ import createClientConfig from '../webpack/client';
|
|||
import createServerConfig from '../webpack/server';
|
||||
import {compile, applyConfigureWebpack} from '../webpack/utils';
|
||||
import CleanWebpackPlugin from '../webpack/plugins/CleanWebpackPlugin';
|
||||
import {loadI18n} from '../server/i18n';
|
||||
import {mapAsyncSequencial} from '@docusaurus/utils';
|
||||
import loadConfig from '../server/config';
|
||||
|
||||
export default async function build(
|
||||
siteDir: string,
|
||||
cliOptions: Partial<BuildCLIOptions> = {},
|
||||
forceTerminate: boolean = true,
|
||||
): Promise<string> {
|
||||
async function tryToBuildLocale(locale: string, forceTerm) {
|
||||
try {
|
||||
const result = await buildLocale(siteDir, locale, cliOptions, forceTerm);
|
||||
console.log(chalk.green(`Site successfully built in locale=${locale}`));
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error(`error building locale=${locale}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const i18n = await loadI18n(loadConfig(siteDir), {
|
||||
locale: cliOptions.locale,
|
||||
});
|
||||
if (cliOptions.locale) {
|
||||
return tryToBuildLocale(cliOptions.locale, forceTerminate);
|
||||
} else {
|
||||
if (i18n.locales.length > 1) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`\nSite will be built with all these locales:
|
||||
- ${i18n.locales.join('\n- ')}\n`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// We need the default locale to always be the 1st in the list
|
||||
// If we build it last, it would "erase" the localized sites built in subfolders
|
||||
const orderedLocales: string[] = [
|
||||
i18n.defaultLocale,
|
||||
...i18n.locales.filter((locale) => locale !== i18n.defaultLocale),
|
||||
];
|
||||
|
||||
const results = await mapAsyncSequencial(orderedLocales, (locale) => {
|
||||
const isLastLocale =
|
||||
i18n.locales.indexOf(locale) === i18n.locales.length - 1;
|
||||
// TODO check why we need forceTerminate
|
||||
const forceTerm = isLastLocale && forceTerminate;
|
||||
return tryToBuildLocale(locale, forceTerm);
|
||||
});
|
||||
return results[0]!;
|
||||
}
|
||||
}
|
||||
|
||||
async function buildLocale(
|
||||
siteDir: string,
|
||||
locale: string,
|
||||
cliOptions: Partial<BuildCLIOptions> = {},
|
||||
forceTerminate: boolean = true,
|
||||
): Promise<string> {
|
||||
process.env.BABEL_ENV = 'production';
|
||||
process.env.NODE_ENV = 'production';
|
||||
console.log(chalk.blue('Creating an optimized production build...'));
|
||||
console.log(
|
||||
chalk.blue(`[${locale}] Creating an optimized production build...`),
|
||||
);
|
||||
|
||||
const props: Props = await load(siteDir, cliOptions.outDir);
|
||||
const props: Props = await load(siteDir, {
|
||||
customOutDir: cliOptions.outDir,
|
||||
locale,
|
||||
localizePath: cliOptions.locale ? false : undefined,
|
||||
});
|
||||
|
||||
// Apply user webpack config.
|
||||
const {
|
||||
|
|
|
@ -18,7 +18,9 @@ export default async function deploy(
|
|||
siteDir: string,
|
||||
cliOptions: Partial<BuildCLIOptions> = {},
|
||||
): Promise<void> {
|
||||
const {outDir} = loadContext(siteDir, cliOptions.outDir);
|
||||
const {outDir} = await loadContext(siteDir, {
|
||||
customOutDir: cliOptions.outDir,
|
||||
});
|
||||
const tempDir = path.join(siteDir, GENERATED_FILES_DIR_NAME);
|
||||
|
||||
console.log('Deploy command invoked ...');
|
||||
|
|
|
@ -9,11 +9,11 @@ import {CommanderStatic} from 'commander';
|
|||
import {loadContext, loadPluginConfigs} from '../server';
|
||||
import initPlugins from '../server/plugins/init';
|
||||
|
||||
export default function externalCommand(
|
||||
export default async function externalCommand(
|
||||
cli: CommanderStatic,
|
||||
siteDir: string,
|
||||
): void {
|
||||
const context = loadContext(siteDir);
|
||||
): Promise<void> {
|
||||
const context = await loadContext(siteDir);
|
||||
const pluginConfigs = loadPluginConfigs(context);
|
||||
const plugins = initPlugins({pluginConfigs, context});
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import {CONFIG_FILE_NAME, STATIC_DIR_NAME} from '../constants';
|
|||
import createClientConfig from '../webpack/client';
|
||||
import {applyConfigureWebpack, getHttpsConfig} from '../webpack/utils';
|
||||
import {getCLIOptionHost, getCLIOptionPort} from './commandUtils';
|
||||
import {getTranslationsLocaleDirPath} from '../server/translations/translations';
|
||||
|
||||
export default async function start(
|
||||
siteDir: string,
|
||||
|
@ -34,8 +35,15 @@ export default async function start(
|
|||
process.env.BABEL_ENV = 'development';
|
||||
console.log(chalk.blue('Starting the development server...'));
|
||||
|
||||
function loadSite() {
|
||||
return load(siteDir, {
|
||||
locale: cliOptions.locale,
|
||||
localizePath: undefined, // should this be configurable?
|
||||
});
|
||||
}
|
||||
|
||||
// Process all related files as a prop.
|
||||
const props = await load(siteDir);
|
||||
const props = await loadSite();
|
||||
|
||||
const protocol: string = process.env.HTTPS === 'true' ? 'https' : 'http';
|
||||
|
||||
|
@ -54,7 +62,7 @@ export default async function start(
|
|||
|
||||
// Reload files processing.
|
||||
const reload = () => {
|
||||
load(siteDir)
|
||||
loadSite()
|
||||
.then(({baseUrl: newBaseUrl}) => {
|
||||
const newOpenUrl = normalizeUrl([urls.localUrlForBrowser, newBaseUrl]);
|
||||
console.log(
|
||||
|
@ -82,7 +90,16 @@ export default async function start(
|
|||
)
|
||||
.map(normalizeToSiteDir);
|
||||
|
||||
const fsWatcher = chokidar.watch([...pluginPaths, CONFIG_FILE_NAME], {
|
||||
const pathsToWatch: string[] = [
|
||||
...pluginPaths,
|
||||
CONFIG_FILE_NAME,
|
||||
getTranslationsLocaleDirPath({
|
||||
siteDir,
|
||||
locale: props.i18n.currentLocale,
|
||||
}),
|
||||
];
|
||||
|
||||
const fsWatcher = chokidar.watch(pathsToWatch, {
|
||||
cwd: siteDir,
|
||||
ignoreInitial: true,
|
||||
usePolling: !!cliOptions.poll,
|
||||
|
|
|
@ -130,7 +130,7 @@ export default async function swizzle(
|
|||
typescript?: boolean,
|
||||
danger?: boolean,
|
||||
): Promise<void> {
|
||||
const context = loadContext(siteDir);
|
||||
const context = await loadContext(siteDir);
|
||||
const pluginConfigs = loadPluginConfigs(context);
|
||||
const pluginNames = getPluginNames(pluginConfigs);
|
||||
const plugins = initPlugins({
|
||||
|
|
82
packages/docusaurus/src/commands/writeTranslations.ts
Normal file
82
packages/docusaurus/src/commands/writeTranslations.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* 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 {loadContext, loadPluginConfigs} from '../server';
|
||||
import initPlugins, {InitPlugin} from '../server/plugins/init';
|
||||
|
||||
import {
|
||||
writePluginTranslations,
|
||||
writeCodeTranslations,
|
||||
WriteTranslationsOptions,
|
||||
} from '../server/translations/translations';
|
||||
import {extractPluginsSourceCodeTranslations} from '../server/translations/translationsExtractor';
|
||||
import {getCustomBabelConfigFilePath, getBabelOptions} from '../webpack/utils';
|
||||
|
||||
async function writePluginTranslationFiles({
|
||||
siteDir,
|
||||
plugin,
|
||||
locale,
|
||||
options,
|
||||
}: {
|
||||
siteDir: string;
|
||||
plugin: InitPlugin;
|
||||
locale: string;
|
||||
options: WriteTranslationsOptions;
|
||||
}) {
|
||||
if (plugin.getTranslationFiles) {
|
||||
const translationFiles = await plugin.getTranslationFiles();
|
||||
|
||||
await Promise.all(
|
||||
translationFiles.map(async (translationFile) => {
|
||||
await writePluginTranslations({
|
||||
siteDir,
|
||||
plugin,
|
||||
translationFile,
|
||||
locale,
|
||||
options,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default async function writeTranslations(
|
||||
siteDir: string,
|
||||
options: WriteTranslationsOptions & {locale?: string},
|
||||
): Promise<void> {
|
||||
const context = await loadContext(siteDir);
|
||||
const pluginConfigs = loadPluginConfigs(context);
|
||||
const plugins = initPlugins({
|
||||
pluginConfigs,
|
||||
context,
|
||||
});
|
||||
|
||||
const locale = options.locale ?? context.i18n.defaultLocale;
|
||||
|
||||
if (!context.i18n.locales.includes(locale)) {
|
||||
throw new Error(
|
||||
`Can't write-translation for locale that is not in the locale configuration file.
|
||||
Unknown locale=[${locale}].
|
||||
Available locales=[${context.i18n.locales.join(',')}]`,
|
||||
);
|
||||
}
|
||||
|
||||
const babelOptions = getBabelOptions({
|
||||
isServer: true,
|
||||
babelOptions: getCustomBabelConfigFilePath(siteDir),
|
||||
});
|
||||
const codeTranslations = await extractPluginsSourceCodeTranslations(
|
||||
plugins,
|
||||
babelOptions,
|
||||
);
|
||||
await writeCodeTranslations({siteDir, locale}, codeTranslations, options);
|
||||
|
||||
await Promise.all(
|
||||
plugins.map(async (plugin) => {
|
||||
await writePluginTranslationFiles({siteDir, plugin, locale, options});
|
||||
}),
|
||||
);
|
||||
}
|
|
@ -12,3 +12,4 @@ export {default as deploy} from './commands/deploy';
|
|||
export {default as externalCommand} from './commands/external';
|
||||
export {default as serve} from './commands/serve';
|
||||
export {default as clear} from './commands/clear';
|
||||
export {default as writeTranslations} from './commands/writeTranslations';
|
||||
|
|
|
@ -21,6 +21,12 @@ Object {
|
|||
"baseUrlIssueBanner": true,
|
||||
"customFields": Object {},
|
||||
"favicon": "img/docusaurus.ico",
|
||||
"i18n": Object {
|
||||
"defaultLocale": "en",
|
||||
"locales": Array [
|
||||
"en",
|
||||
],
|
||||
},
|
||||
"noIndex": false,
|
||||
"onBrokenLinks": "throw",
|
||||
"onBrokenMarkdownLinks": "warn",
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`loadI18n should throw when trying to load undeclared locale 1`] = `
|
||||
"It is not possible to load Docusaurus with locale=\\"it\\".
|
||||
This locale is not in the available locales of your site configuration: config.i18n.locales=[en,fr,de]
|
||||
Note: Docusaurus only support running one local at a time."
|
||||
`;
|
156
packages/docusaurus/src/server/__tests__/i18n.test.ts
Normal file
156
packages/docusaurus/src/server/__tests__/i18n.test.ts
Normal file
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* 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 {loadI18n, localizePath} from '../i18n';
|
||||
import {DEFAULT_I18N_CONFIG} from '../configValidation';
|
||||
import path from 'path';
|
||||
|
||||
describe('loadI18n', () => {
|
||||
test('should load I18n for default config', async () => {
|
||||
await expect(
|
||||
loadI18n(
|
||||
// @ts-expect-error: enough for this test
|
||||
{
|
||||
i18n: DEFAULT_I18N_CONFIG,
|
||||
},
|
||||
),
|
||||
).resolves.toEqual({
|
||||
defaultLocale: 'en',
|
||||
locales: ['en'],
|
||||
currentLocale: 'en',
|
||||
});
|
||||
});
|
||||
|
||||
test('should load I18n for multi-lang config', async () => {
|
||||
await expect(
|
||||
loadI18n(
|
||||
// @ts-expect-error: enough for this test
|
||||
{
|
||||
i18n: {
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
},
|
||||
},
|
||||
),
|
||||
).resolves.toEqual({
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
currentLocale: 'fr',
|
||||
});
|
||||
});
|
||||
|
||||
test('should load I18n for multi-locale config with specified locale', async () => {
|
||||
await expect(
|
||||
loadI18n(
|
||||
// @ts-expect-error: enough for this test
|
||||
{
|
||||
i18n: {
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
},
|
||||
},
|
||||
{locale: 'de'},
|
||||
),
|
||||
).resolves.toEqual({
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
currentLocale: 'de',
|
||||
});
|
||||
});
|
||||
|
||||
test('should throw when trying to load undeclared locale', async () => {
|
||||
await expect(
|
||||
loadI18n(
|
||||
// @ts-expect-error: enough for this test
|
||||
{
|
||||
i18n: {
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
},
|
||||
},
|
||||
{locale: 'it'},
|
||||
),
|
||||
).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('localizePath', () => {
|
||||
test('should localize url path with current locale', () => {
|
||||
expect(
|
||||
localizePath({
|
||||
pathType: 'url',
|
||||
path: '/baseUrl',
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'fr'],
|
||||
currentLocale: 'fr',
|
||||
},
|
||||
options: {localizePath: true},
|
||||
}),
|
||||
).toEqual('/baseUrl/fr/');
|
||||
});
|
||||
|
||||
test('should localize fs path with current locale', () => {
|
||||
expect(
|
||||
localizePath({
|
||||
pathType: 'fs',
|
||||
path: '/baseFsPath',
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'fr'],
|
||||
currentLocale: 'fr',
|
||||
},
|
||||
options: {localizePath: true},
|
||||
}),
|
||||
).toEqual(`/baseFsPath${path.sep}fr${path.sep}`);
|
||||
});
|
||||
|
||||
test('should localize path for default locale, if requested', () => {
|
||||
expect(
|
||||
localizePath({
|
||||
pathType: 'url',
|
||||
path: '/baseUrl/',
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'fr'],
|
||||
currentLocale: 'en',
|
||||
},
|
||||
options: {localizePath: true},
|
||||
}),
|
||||
).toEqual('/baseUrl/en/');
|
||||
});
|
||||
|
||||
test('should not localize path for default locale by default', () => {
|
||||
expect(
|
||||
localizePath({
|
||||
pathType: 'url',
|
||||
path: '/baseUrl/',
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'fr'],
|
||||
currentLocale: 'en',
|
||||
},
|
||||
// options: {localizePath: true},
|
||||
}),
|
||||
).toEqual('/baseUrl/');
|
||||
});
|
||||
|
||||
test('should localize path for non-default locale by default', () => {
|
||||
expect(
|
||||
localizePath({
|
||||
pathType: 'url',
|
||||
path: '/baseUrl/',
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'fr'],
|
||||
currentLocale: 'en',
|
||||
},
|
||||
// options: {localizePath: true},
|
||||
}),
|
||||
).toEqual('/baseUrl/');
|
||||
});
|
||||
});
|
|
@ -5,7 +5,7 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {DocusaurusConfig} from '@docusaurus/types';
|
||||
import {DocusaurusConfig, I18nConfig} from '@docusaurus/types';
|
||||
import {CONFIG_FILE_NAME} from '../constants';
|
||||
import Joi from 'joi';
|
||||
import {
|
||||
|
@ -14,8 +14,16 @@ import {
|
|||
URISchema,
|
||||
} from '@docusaurus/utils-validation';
|
||||
|
||||
const DEFAULT_I18N_LOCALE = 'en';
|
||||
|
||||
export const DEFAULT_I18N_CONFIG: I18nConfig = {
|
||||
defaultLocale: DEFAULT_I18N_LOCALE,
|
||||
locales: [DEFAULT_I18N_LOCALE],
|
||||
};
|
||||
|
||||
export const DEFAULT_CONFIG: Pick<
|
||||
DocusaurusConfig,
|
||||
| 'i18n'
|
||||
| 'onBrokenLinks'
|
||||
| 'onBrokenMarkdownLinks'
|
||||
| 'onDuplicateRoutes'
|
||||
|
@ -28,6 +36,7 @@ export const DEFAULT_CONFIG: Pick<
|
|||
| 'noIndex'
|
||||
| 'baseUrlIssueBanner'
|
||||
> = {
|
||||
i18n: DEFAULT_I18N_CONFIG,
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
onDuplicateRoutes: 'warn',
|
||||
|
@ -57,6 +66,13 @@ const PresetSchema = Joi.alternatives().try(
|
|||
Joi.array().items(Joi.string().required(), Joi.object().required()).length(2),
|
||||
);
|
||||
|
||||
const I18N_CONFIG_SCHEMA = Joi.object<I18nConfig>({
|
||||
defaultLocale: Joi.string().required(),
|
||||
locales: Joi.array().items().min(1).items(Joi.string().required()).required(),
|
||||
})
|
||||
.optional()
|
||||
.default(DEFAULT_I18N_CONFIG);
|
||||
|
||||
// TODO move to @docusaurus/utils-validation
|
||||
const ConfigSchema = Joi.object({
|
||||
baseUrl: Joi.string()
|
||||
|
@ -67,6 +83,7 @@ const ConfigSchema = Joi.object({
|
|||
favicon: Joi.string().required(),
|
||||
title: Joi.string().required(),
|
||||
url: URISchema.required(),
|
||||
i18n: I18N_CONFIG_SCHEMA,
|
||||
onBrokenLinks: Joi.string()
|
||||
.equal('ignore', 'log', 'warn', 'error', 'throw')
|
||||
.default(DEFAULT_CONFIG.onBrokenLinks),
|
||||
|
|
67
packages/docusaurus/src/server/i18n.ts
Normal file
67
packages/docusaurus/src/server/i18n.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* 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 {I18n, DocusaurusConfig} from '@docusaurus/types';
|
||||
import path from 'path';
|
||||
import {normalizeUrl} from '@docusaurus/utils';
|
||||
|
||||
export async function loadI18n(
|
||||
config: DocusaurusConfig,
|
||||
options: {locale?: string} = {},
|
||||
): Promise<I18n> {
|
||||
const i18nConfig = config.i18n;
|
||||
const currentLocale = options.locale ?? i18nConfig.defaultLocale;
|
||||
|
||||
if (currentLocale && !i18nConfig.locales.includes(currentLocale)) {
|
||||
throw new Error(
|
||||
`It is not possible to load Docusaurus with locale="${currentLocale}".
|
||||
This locale is not in the available locales of your site configuration: config.i18n.locales=[${i18nConfig.locales.join(
|
||||
',',
|
||||
)}]
|
||||
Note: Docusaurus only support running one local at a time.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...i18nConfig,
|
||||
currentLocale,
|
||||
};
|
||||
}
|
||||
|
||||
export function localizePath({
|
||||
pathType,
|
||||
path: originalPath,
|
||||
i18n,
|
||||
options = {},
|
||||
}: {
|
||||
pathType: 'fs' | 'url';
|
||||
path: string;
|
||||
i18n: I18n;
|
||||
options?: {localizePath?: boolean};
|
||||
}): string {
|
||||
const shouldLocalizePath: boolean =
|
||||
typeof options.localizePath === 'undefined'
|
||||
? // By default, we don't localize the path of defaultLocale
|
||||
i18n.currentLocale !== i18n.defaultLocale
|
||||
: options.localizePath;
|
||||
|
||||
if (shouldLocalizePath) {
|
||||
// FS paths need special care, for Windows support
|
||||
if (pathType === 'fs') {
|
||||
return path.join(originalPath, path.sep, i18n.currentLocale, path.sep);
|
||||
}
|
||||
// Url paths
|
||||
else if (pathType === 'url') {
|
||||
return normalizeUrl([originalPath, '/', i18n.currentLocale, '/']);
|
||||
}
|
||||
// should never happen
|
||||
else {
|
||||
throw new Error(`unhandled pathType=${pathType}`);
|
||||
}
|
||||
} else {
|
||||
return originalPath;
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import {generate} from '@docusaurus/utils';
|
||||
import path, {join} from 'path';
|
||||
import chalk from 'chalk';
|
||||
import ssrDefaultTemplate from '../client/templates/ssr.html.template';
|
||||
import {
|
||||
BUILD_DIR_NAME,
|
||||
|
@ -30,21 +31,60 @@ import {
|
|||
import {loadHtmlTags} from './html-tags';
|
||||
import {getPackageJsonVersion} from './versions';
|
||||
import {handleDuplicateRoutes} from './duplicateRoutes';
|
||||
import chalk from 'chalk';
|
||||
import {loadI18n, localizePath} from './i18n';
|
||||
import {readCodeTranslationFileContent} from './translations/translations';
|
||||
import {mapValues} from 'lodash';
|
||||
|
||||
export function loadContext(
|
||||
type LoadContextOptions = {
|
||||
customOutDir?: string;
|
||||
locale?: string;
|
||||
localizePath?: boolean; // undefined = only non-default locales paths are localized
|
||||
};
|
||||
|
||||
export async function loadContext(
|
||||
siteDir: string,
|
||||
customOutDir?: string,
|
||||
): LoadContext {
|
||||
options: LoadContextOptions = {},
|
||||
): Promise<LoadContext> {
|
||||
const {customOutDir, locale} = options;
|
||||
const generatedFilesDir: string = path.resolve(
|
||||
siteDir,
|
||||
GENERATED_FILES_DIR_NAME,
|
||||
);
|
||||
const siteConfig: DocusaurusConfig = loadConfig(siteDir);
|
||||
const outDir = customOutDir
|
||||
const initialSiteConfig: DocusaurusConfig = loadConfig(siteDir);
|
||||
const {ssrTemplate} = initialSiteConfig;
|
||||
|
||||
const baseOutDir = customOutDir
|
||||
? path.resolve(customOutDir)
|
||||
: path.resolve(siteDir, BUILD_DIR_NAME);
|
||||
const {baseUrl, ssrTemplate} = siteConfig;
|
||||
|
||||
const i18n = await loadI18n(initialSiteConfig, {locale});
|
||||
|
||||
const baseUrl = localizePath({
|
||||
path: initialSiteConfig.baseUrl,
|
||||
i18n,
|
||||
options,
|
||||
pathType: 'url',
|
||||
});
|
||||
const outDir = localizePath({
|
||||
path: baseOutDir,
|
||||
i18n,
|
||||
options,
|
||||
pathType: 'fs',
|
||||
});
|
||||
|
||||
const siteConfig: DocusaurusConfig = {...initialSiteConfig, baseUrl};
|
||||
|
||||
const codeTranslationFileContent =
|
||||
(await readCodeTranslationFileContent({
|
||||
siteDir,
|
||||
locale: i18n.currentLocale,
|
||||
})) ?? {};
|
||||
|
||||
// We only need key->message for code translations
|
||||
const codeTranslations = mapValues(
|
||||
codeTranslationFileContent,
|
||||
(value) => value.message,
|
||||
);
|
||||
|
||||
return {
|
||||
siteDir,
|
||||
|
@ -52,7 +92,9 @@ export function loadContext(
|
|||
siteConfig,
|
||||
outDir,
|
||||
baseUrl,
|
||||
i18n,
|
||||
ssrTemplate,
|
||||
codeTranslations,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -70,19 +112,34 @@ export function loadPluginConfigs(context: LoadContext): PluginConfig[] {
|
|||
|
||||
export async function load(
|
||||
siteDir: string,
|
||||
customOutDir?: string,
|
||||
options: LoadContextOptions = {},
|
||||
): Promise<Props> {
|
||||
// Context.
|
||||
const context: LoadContext = loadContext(siteDir, customOutDir);
|
||||
const {generatedFilesDir, siteConfig, outDir, baseUrl, ssrTemplate} = context;
|
||||
|
||||
const context: LoadContext = await loadContext(siteDir, options);
|
||||
const {
|
||||
generatedFilesDir,
|
||||
siteConfig,
|
||||
outDir,
|
||||
baseUrl,
|
||||
i18n,
|
||||
ssrTemplate,
|
||||
codeTranslations,
|
||||
} = context;
|
||||
// Plugins.
|
||||
const pluginConfigs: PluginConfig[] = loadPluginConfigs(context);
|
||||
const {plugins, pluginsRouteConfigs, globalData} = await loadPlugins({
|
||||
const {
|
||||
plugins,
|
||||
pluginsRouteConfigs,
|
||||
globalData,
|
||||
themeConfigTranslated,
|
||||
} = await loadPlugins({
|
||||
pluginConfigs,
|
||||
context,
|
||||
});
|
||||
|
||||
// Side-effect to replace the untranslated themeConfig by the translated one
|
||||
context.siteConfig.themeConfig = themeConfigTranslated;
|
||||
|
||||
handleDuplicateRoutes(pluginsRouteConfigs, siteConfig.onDuplicateRoutes);
|
||||
|
||||
// Site config must be generated after plugins
|
||||
|
@ -204,6 +261,18 @@ ${Object.keys(registry)
|
|||
JSON.stringify(globalData, null, 2),
|
||||
);
|
||||
|
||||
const genI18n = generate(
|
||||
generatedFilesDir,
|
||||
'i18n.json',
|
||||
JSON.stringify(i18n, null, 2),
|
||||
);
|
||||
|
||||
const genCodeTranslations = generate(
|
||||
generatedFilesDir,
|
||||
'codeTranslations.json',
|
||||
JSON.stringify(codeTranslations, null, 2),
|
||||
);
|
||||
|
||||
// Version metadata.
|
||||
const siteMetadata: DocusaurusSiteMetadata = {
|
||||
docusaurusVersion: getPackageJsonVersion(
|
||||
|
@ -232,6 +301,8 @@ ${Object.keys(registry)
|
|||
genRoutes,
|
||||
genGlobalData,
|
||||
genSiteMetadata,
|
||||
genI18n,
|
||||
genCodeTranslations,
|
||||
]);
|
||||
|
||||
const props: Props = {
|
||||
|
@ -239,6 +310,7 @@ ${Object.keys(registry)
|
|||
siteDir,
|
||||
outDir,
|
||||
baseUrl,
|
||||
i18n,
|
||||
generatedFilesDir,
|
||||
routes: pluginsRouteConfigs,
|
||||
routesPaths,
|
||||
|
@ -247,6 +319,7 @@ ${Object.keys(registry)
|
|||
preBodyTags,
|
||||
postBodyTags,
|
||||
ssrTemplate: ssrTemplate || ssrDefaultTemplate,
|
||||
codeTranslations,
|
||||
};
|
||||
|
||||
return props;
|
||||
|
|
|
@ -9,16 +9,19 @@ import {generate} from '@docusaurus/utils';
|
|||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import {
|
||||
AllContent,
|
||||
LoadContext,
|
||||
PluginConfig,
|
||||
PluginContentLoadedActions,
|
||||
RouteConfig,
|
||||
AllContent,
|
||||
TranslationFiles,
|
||||
ThemeConfig,
|
||||
} from '@docusaurus/types';
|
||||
import initPlugins, {InitPlugin} from './init';
|
||||
import chalk from 'chalk';
|
||||
import {DEFAULT_PLUGIN_ID} from '../../constants';
|
||||
import {chain} from 'lodash';
|
||||
import {localizePluginTranslationFile} from '../translations/translations';
|
||||
|
||||
export function sortConfig(routeConfigs: RouteConfig[]): void {
|
||||
// Sort the route config. This ensures that route with nested
|
||||
|
@ -49,6 +52,14 @@ export function sortConfig(routeConfigs: RouteConfig[]): void {
|
|||
});
|
||||
}
|
||||
|
||||
export type AllPluginsTranslationFiles = Record<
|
||||
string, // plugin name
|
||||
Record<
|
||||
string, // plugin id
|
||||
TranslationFiles
|
||||
>
|
||||
>;
|
||||
|
||||
export async function loadPlugins({
|
||||
pluginConfigs,
|
||||
context,
|
||||
|
@ -59,6 +70,7 @@ export async function loadPlugins({
|
|||
plugins: InitPlugin[];
|
||||
pluginsRouteConfigs: RouteConfig[];
|
||||
globalData: any;
|
||||
themeConfigTranslated: ThemeConfig;
|
||||
}> {
|
||||
// 1. Plugin Lifecycle - Initialization/Constructor.
|
||||
const plugins: InitPlugin[] = initPlugins({
|
||||
|
@ -78,6 +90,30 @@ export async function loadPlugins({
|
|||
}),
|
||||
);
|
||||
|
||||
type ContentLoadedTranslatedPlugin = ContentLoadedPlugin & {
|
||||
translationFiles: TranslationFiles;
|
||||
};
|
||||
const contentLoadedTranslatedPlugins: ContentLoadedTranslatedPlugin[] = await Promise.all(
|
||||
contentLoadedPlugins.map(async (contentLoadedPlugin) => {
|
||||
const translationFiles =
|
||||
(await contentLoadedPlugin.plugin?.getTranslationFiles?.()) ?? [];
|
||||
const localizedTranslationFiles = await Promise.all(
|
||||
translationFiles.map((translationFile) =>
|
||||
localizePluginTranslationFile({
|
||||
locale: context.i18n.currentLocale,
|
||||
siteDir: context.siteDir,
|
||||
translationFile,
|
||||
plugin: contentLoadedPlugin.plugin,
|
||||
}),
|
||||
),
|
||||
);
|
||||
return {
|
||||
...contentLoadedPlugin,
|
||||
translationFiles: localizedTranslationFiles,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const allContent: AllContent = chain(contentLoadedPlugins)
|
||||
.groupBy((item) => item.plugin.name)
|
||||
.mapValues((nameItems) => {
|
||||
|
@ -94,52 +130,57 @@ export async function loadPlugins({
|
|||
const globalData = {};
|
||||
|
||||
await Promise.all(
|
||||
contentLoadedPlugins.map(async ({plugin, content}) => {
|
||||
if (!plugin.contentLoaded) {
|
||||
return;
|
||||
}
|
||||
contentLoadedTranslatedPlugins.map(
|
||||
async ({plugin, content, translationFiles}) => {
|
||||
if (!plugin.contentLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pluginId = plugin.options.id ?? DEFAULT_PLUGIN_ID;
|
||||
const pluginId = plugin.options.id ?? DEFAULT_PLUGIN_ID;
|
||||
|
||||
// plugins data files are namespaced by pluginName/pluginId
|
||||
const dataDirRoot = path.join(context.generatedFilesDir, plugin.name);
|
||||
const dataDir = path.join(dataDirRoot, pluginId);
|
||||
// plugins data files are namespaced by pluginName/pluginId
|
||||
const dataDirRoot = path.join(context.generatedFilesDir, plugin.name);
|
||||
const dataDir = path.join(dataDirRoot, pluginId);
|
||||
|
||||
const addRoute: PluginContentLoadedActions['addRoute'] = (config) =>
|
||||
pluginsRouteConfigs.push(config);
|
||||
const addRoute: PluginContentLoadedActions['addRoute'] = (config) =>
|
||||
pluginsRouteConfigs.push(config);
|
||||
|
||||
const createData: PluginContentLoadedActions['createData'] = async (
|
||||
name,
|
||||
data,
|
||||
) => {
|
||||
const modulePath = path.join(dataDir, name);
|
||||
await fs.ensureDir(path.dirname(modulePath));
|
||||
await generate(dataDir, name, data);
|
||||
return modulePath;
|
||||
};
|
||||
const createData: PluginContentLoadedActions['createData'] = async (
|
||||
name,
|
||||
data,
|
||||
) => {
|
||||
const modulePath = path.join(dataDir, name);
|
||||
await fs.ensureDir(path.dirname(modulePath));
|
||||
await generate(dataDir, name, data);
|
||||
return modulePath;
|
||||
};
|
||||
|
||||
// the plugins global data are namespaced to avoid data conflicts:
|
||||
// - by plugin name
|
||||
// - by plugin id (allow using multiple instances of the same plugin)
|
||||
const setGlobalData: PluginContentLoadedActions['setGlobalData'] = (
|
||||
data,
|
||||
) => {
|
||||
globalData[plugin.name] = globalData[plugin.name] ?? {};
|
||||
globalData[plugin.name][pluginId] = data;
|
||||
};
|
||||
// the plugins global data are namespaced to avoid data conflicts:
|
||||
// - by plugin name
|
||||
// - by plugin id (allow using multiple instances of the same plugin)
|
||||
const setGlobalData: PluginContentLoadedActions['setGlobalData'] = (
|
||||
data,
|
||||
) => {
|
||||
globalData[plugin.name] = globalData[plugin.name] ?? {};
|
||||
globalData[plugin.name][pluginId] = data;
|
||||
};
|
||||
|
||||
const actions: PluginContentLoadedActions = {
|
||||
addRoute,
|
||||
createData,
|
||||
setGlobalData,
|
||||
};
|
||||
const actions: PluginContentLoadedActions = {
|
||||
addRoute,
|
||||
createData,
|
||||
setGlobalData,
|
||||
};
|
||||
|
||||
await plugin.contentLoaded({
|
||||
content,
|
||||
actions,
|
||||
allContent,
|
||||
});
|
||||
}),
|
||||
const translatedContent =
|
||||
plugin.translateContent?.({content, translationFiles}) ?? content;
|
||||
|
||||
await plugin.contentLoaded({
|
||||
content: translatedContent,
|
||||
actions,
|
||||
allContent,
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// 4. Plugin Lifecycle - routesLoaded.
|
||||
|
@ -147,7 +188,7 @@ export async function loadPlugins({
|
|||
// We could change this in future if there are plugins which need to
|
||||
// run in certain order or depend on others for data.
|
||||
await Promise.all(
|
||||
plugins.map(async (plugin) => {
|
||||
contentLoadedTranslatedPlugins.map(async ({plugin}) => {
|
||||
if (!plugin.routesLoaded) {
|
||||
return null;
|
||||
}
|
||||
|
@ -168,9 +209,29 @@ export async function loadPlugins({
|
|||
// routes are always placed last.
|
||||
sortConfig(pluginsRouteConfigs);
|
||||
|
||||
// Apply each plugin one after the other to translate the theme config
|
||||
function translateThemeConfig(
|
||||
untranslatedThemeConfig: ThemeConfig,
|
||||
): ThemeConfig {
|
||||
return contentLoadedTranslatedPlugins.reduce(
|
||||
(currentThemeConfig, {plugin, translationFiles}) => {
|
||||
const translatedThemeConfigSlice = plugin.translateThemeConfig?.({
|
||||
themeConfig: currentThemeConfig,
|
||||
translationFiles,
|
||||
});
|
||||
return {
|
||||
...currentThemeConfig,
|
||||
...translatedThemeConfigSlice,
|
||||
};
|
||||
},
|
||||
untranslatedThemeConfig,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
plugins,
|
||||
pluginsRouteConfigs,
|
||||
globalData,
|
||||
themeConfigTranslated: translateThemeConfig(context.siteConfig.themeConfig),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,465 @@
|
|||
/**
|
||||
* 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 {
|
||||
ensureTranslationFileContent,
|
||||
writeTranslationFileContent,
|
||||
writePluginTranslations,
|
||||
readTranslationFileContent,
|
||||
WriteTranslationsOptions,
|
||||
localizePluginTranslationFile,
|
||||
} from '../translations';
|
||||
import fs from 'fs-extra';
|
||||
import tmp from 'tmp-promise';
|
||||
import {TranslationFile, TranslationFileContent} from '@docusaurus/types';
|
||||
import path from 'path';
|
||||
|
||||
async function createTmpSiteDir() {
|
||||
const {path: siteDirPath} = await tmp.dir({
|
||||
prefix: 'jest-createTmpSiteDir',
|
||||
});
|
||||
return siteDirPath;
|
||||
}
|
||||
|
||||
async function createTmpTranslationFile(
|
||||
content: TranslationFileContent | null,
|
||||
) {
|
||||
const file = await tmp.file({
|
||||
prefix: 'jest-createTmpTranslationFile',
|
||||
postfix: '.json',
|
||||
});
|
||||
|
||||
// null means we don't want a file, but tmp.file() creates an empty file :(
|
||||
if (content === null) {
|
||||
await fs.unlink(file.path);
|
||||
} else {
|
||||
await fs.writeFile(file.path, JSON.stringify(content, null, 2));
|
||||
}
|
||||
|
||||
return {
|
||||
filePath: file.path,
|
||||
readFile: async () => JSON.parse(await fs.readFile(file.path, 'utf8')),
|
||||
};
|
||||
}
|
||||
|
||||
describe('ensureTranslationFileContent', () => {
|
||||
test('should pass valid translation file content', () => {
|
||||
ensureTranslationFileContent({});
|
||||
ensureTranslationFileContent({key1: {message: ''}});
|
||||
ensureTranslationFileContent({key1: {message: 'abc'}});
|
||||
ensureTranslationFileContent({key1: {message: 'abc', description: 'desc'}});
|
||||
ensureTranslationFileContent({
|
||||
key1: {message: 'abc', description: 'desc'},
|
||||
key2: {message: 'def', description: 'desc'},
|
||||
});
|
||||
});
|
||||
|
||||
test('should fail for invalid translation file content', () => {
|
||||
expect(() =>
|
||||
ensureTranslationFileContent(null),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"\\"value\\" must be of type object"`,
|
||||
);
|
||||
expect(() =>
|
||||
ensureTranslationFileContent(undefined),
|
||||
).toThrowErrorMatchingInlineSnapshot(`"\\"value\\" is required"`);
|
||||
expect(() =>
|
||||
ensureTranslationFileContent('HEY'),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"\\"value\\" must be of type object"`,
|
||||
);
|
||||
expect(() =>
|
||||
ensureTranslationFileContent(42),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"\\"value\\" must be of type object"`,
|
||||
);
|
||||
expect(() =>
|
||||
ensureTranslationFileContent({key: {description: 'no message'}}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`"\\"key.message\\" is required"`);
|
||||
expect(() =>
|
||||
ensureTranslationFileContent({key: {message: 42}}),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"\\"key.message\\" must be a string"`,
|
||||
);
|
||||
expect(() =>
|
||||
ensureTranslationFileContent({
|
||||
key: {message: 'Message', description: 42},
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"\\"key.description\\" must be a string"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeTranslationFileContent', () => {
|
||||
test('should create new translation file', async () => {
|
||||
const {filePath, readFile} = await createTmpTranslationFile(null);
|
||||
|
||||
await writeTranslationFileContent({
|
||||
filePath,
|
||||
content: {
|
||||
key1: {message: 'key1 message'},
|
||||
key2: {message: 'key2 message'},
|
||||
key3: {message: 'key3 message'},
|
||||
},
|
||||
});
|
||||
|
||||
expect(await readFile()).toEqual({
|
||||
key1: {message: 'key1 message'},
|
||||
key2: {message: 'key2 message'},
|
||||
key3: {message: 'key3 message'},
|
||||
});
|
||||
});
|
||||
|
||||
test('should create new translation file with prefix', async () => {
|
||||
const {filePath, readFile} = await createTmpTranslationFile(null);
|
||||
|
||||
await writeTranslationFileContent({
|
||||
filePath,
|
||||
content: {
|
||||
key1: {message: 'key1 message'},
|
||||
key2: {message: 'key2 message'},
|
||||
key3: {message: 'key3 message'},
|
||||
},
|
||||
options: {
|
||||
messagePrefix: 'PREFIX ',
|
||||
},
|
||||
});
|
||||
|
||||
expect(await readFile()).toEqual({
|
||||
key1: {message: 'PREFIX key1 message'},
|
||||
key2: {message: 'PREFIX key2 message'},
|
||||
key3: {message: 'PREFIX key3 message'},
|
||||
});
|
||||
});
|
||||
|
||||
test('should append missing translations', async () => {
|
||||
const {filePath, readFile} = await createTmpTranslationFile({
|
||||
key1: {message: 'key1 message'},
|
||||
key2: {message: 'key2 message'},
|
||||
key3: {message: 'key3 message'},
|
||||
});
|
||||
|
||||
await writeTranslationFileContent({
|
||||
filePath,
|
||||
content: {
|
||||
key1: {message: 'key1 message new'},
|
||||
key2: {message: 'key2 message new'},
|
||||
key3: {message: 'key3 message new'},
|
||||
key4: {message: 'key4 message new'},
|
||||
},
|
||||
});
|
||||
|
||||
expect(await readFile()).toEqual({
|
||||
key1: {message: 'key1 message'},
|
||||
key2: {message: 'key2 message'},
|
||||
key3: {message: 'key3 message'},
|
||||
key4: {message: 'key4 message new'},
|
||||
});
|
||||
});
|
||||
|
||||
test('should append missing translations with prefix', async () => {
|
||||
const {filePath, readFile} = await createTmpTranslationFile({
|
||||
key1: {message: 'key1 message'},
|
||||
});
|
||||
|
||||
await writeTranslationFileContent({
|
||||
filePath,
|
||||
content: {
|
||||
key1: {message: 'key1 message new'},
|
||||
key2: {message: 'key2 message new'},
|
||||
},
|
||||
options: {
|
||||
messagePrefix: 'PREFIX ',
|
||||
},
|
||||
});
|
||||
|
||||
expect(await readFile()).toEqual({
|
||||
key1: {message: 'key1 message'},
|
||||
key2: {message: 'PREFIX key2 message new'},
|
||||
});
|
||||
});
|
||||
|
||||
test('should override missing translations', async () => {
|
||||
const {filePath, readFile} = await createTmpTranslationFile({
|
||||
key1: {message: 'key1 message'},
|
||||
});
|
||||
|
||||
await writeTranslationFileContent({
|
||||
filePath,
|
||||
content: {
|
||||
key1: {message: 'key1 message new'},
|
||||
key2: {message: 'key2 message new'},
|
||||
},
|
||||
options: {
|
||||
override: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(await readFile()).toEqual({
|
||||
key1: {message: 'key1 message new'},
|
||||
key2: {message: 'key2 message new'},
|
||||
});
|
||||
});
|
||||
|
||||
test('should override missing translations with prefix', async () => {
|
||||
const {filePath, readFile} = await createTmpTranslationFile({
|
||||
key1: {message: 'key1 message'},
|
||||
});
|
||||
|
||||
await writeTranslationFileContent({
|
||||
filePath,
|
||||
content: {
|
||||
key1: {message: 'key1 message new'},
|
||||
key2: {message: 'key2 message new'},
|
||||
},
|
||||
options: {
|
||||
override: true,
|
||||
messagePrefix: 'PREFIX ',
|
||||
},
|
||||
});
|
||||
|
||||
expect(await readFile()).toEqual({
|
||||
key1: {message: 'PREFIX key1 message new'},
|
||||
key2: {message: 'PREFIX key2 message new'},
|
||||
});
|
||||
});
|
||||
|
||||
test('should always override message description', async () => {
|
||||
const {filePath, readFile} = await createTmpTranslationFile({
|
||||
key1: {message: 'key1 message', description: 'key1 desc'},
|
||||
key2: {message: 'key2 message', description: 'key2 desc'},
|
||||
key3: {message: 'key3 message', description: undefined},
|
||||
});
|
||||
|
||||
await writeTranslationFileContent({
|
||||
filePath,
|
||||
content: {
|
||||
key1: {message: 'key1 message new', description: undefined},
|
||||
key2: {message: 'key2 message new', description: 'key2 desc new'},
|
||||
key3: {message: 'key3 message new', description: 'key3 desc new'},
|
||||
},
|
||||
});
|
||||
|
||||
expect(await readFile()).toEqual({
|
||||
key1: {message: 'key1 message', description: undefined},
|
||||
key2: {message: 'key2 message', description: 'key2 desc new'},
|
||||
key3: {message: 'key3 message', description: 'key3 desc new'},
|
||||
});
|
||||
});
|
||||
|
||||
test('should always override message description', async () => {
|
||||
const {filePath} = await createTmpTranslationFile(
|
||||
// @ts-expect-error: bad content on purpose
|
||||
{bad: 'content'},
|
||||
);
|
||||
|
||||
await expect(
|
||||
writeTranslationFileContent({
|
||||
filePath,
|
||||
content: {
|
||||
key1: {message: 'key1 message'},
|
||||
},
|
||||
}),
|
||||
).rejects.toThrowError(/Invalid translation file at path/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('writePluginTranslations', () => {
|
||||
test('should write plugin translations', async () => {
|
||||
const siteDir = await createTmpSiteDir();
|
||||
|
||||
const filePath = path.join(
|
||||
siteDir,
|
||||
'i18n',
|
||||
'fr',
|
||||
'my-plugin-name',
|
||||
'my/translation/file.json',
|
||||
);
|
||||
|
||||
await writePluginTranslations({
|
||||
siteDir,
|
||||
locale: 'fr',
|
||||
translationFile: {
|
||||
path: 'my/translation/file',
|
||||
content: {
|
||||
key1: {message: 'key1 message'},
|
||||
key2: {message: 'key2 message'},
|
||||
key3: {message: 'key3 message'},
|
||||
},
|
||||
},
|
||||
// @ts-expect-error: enough for this test
|
||||
plugin: {
|
||||
name: 'my-plugin-name',
|
||||
options: {
|
||||
id: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(await readTranslationFileContent(filePath)).toEqual({
|
||||
key1: {message: 'key1 message'},
|
||||
key2: {message: 'key2 message'},
|
||||
key3: {message: 'key3 message'},
|
||||
});
|
||||
});
|
||||
|
||||
test('should write plugin translations consecutively with different options', async () => {
|
||||
const siteDir = await createTmpSiteDir();
|
||||
|
||||
const filePath = path.join(
|
||||
siteDir,
|
||||
'i18n',
|
||||
'fr',
|
||||
'my-plugin-name-my-plugin-id',
|
||||
'my/translation/file.json',
|
||||
);
|
||||
|
||||
function doWritePluginTranslations(
|
||||
content: TranslationFileContent,
|
||||
options?: WriteTranslationsOptions,
|
||||
) {
|
||||
return writePluginTranslations({
|
||||
siteDir,
|
||||
locale: 'fr',
|
||||
translationFile: {
|
||||
path: 'my/translation/file',
|
||||
content,
|
||||
},
|
||||
// @ts-expect-error: enough for this test
|
||||
plugin: {
|
||||
name: 'my-plugin-name',
|
||||
options: {
|
||||
id: 'my-plugin-id',
|
||||
},
|
||||
},
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
expect(await readTranslationFileContent(filePath)).toEqual(undefined);
|
||||
|
||||
await doWritePluginTranslations({
|
||||
key1: {message: 'key1 message', description: 'key1 desc'},
|
||||
key2: {message: 'key2 message', description: 'key2 desc'},
|
||||
key3: {message: 'key3 message', description: 'key3 desc'},
|
||||
});
|
||||
expect(await readTranslationFileContent(filePath)).toEqual({
|
||||
key1: {message: 'key1 message', description: 'key1 desc'},
|
||||
key2: {message: 'key2 message', description: 'key2 desc'},
|
||||
key3: {message: 'key3 message', description: 'key3 desc'},
|
||||
});
|
||||
|
||||
await doWritePluginTranslations(
|
||||
{
|
||||
key3: {message: 'key3 message 2'},
|
||||
key4: {message: 'key4 message 2', description: 'key4 desc'},
|
||||
},
|
||||
{messagePrefix: 'PREFIX '},
|
||||
);
|
||||
expect(await readTranslationFileContent(filePath)).toEqual({
|
||||
key1: {message: 'key1 message', description: 'key1 desc'},
|
||||
key2: {message: 'key2 message', description: 'key2 desc'},
|
||||
key3: {message: 'key3 message', description: undefined},
|
||||
key4: {message: 'PREFIX key4 message 2', description: 'key4 desc'},
|
||||
});
|
||||
|
||||
await doWritePluginTranslations(
|
||||
{
|
||||
key1: {message: 'key1 message 3', description: 'key1 desc'},
|
||||
key2: {message: 'key2 message 3', description: 'key2 desc'},
|
||||
key3: {message: 'key3 message 3', description: 'key3 desc'},
|
||||
key4: {message: 'key4 message 3', description: 'key4 desc'},
|
||||
},
|
||||
{messagePrefix: 'PREFIX ', override: true},
|
||||
);
|
||||
expect(await readTranslationFileContent(filePath)).toEqual({
|
||||
key1: {message: 'PREFIX key1 message 3', description: 'key1 desc'},
|
||||
key2: {message: 'PREFIX key2 message 3', description: 'key2 desc'},
|
||||
key3: {message: 'PREFIX key3 message 3', description: 'key3 desc'},
|
||||
key4: {message: 'PREFIX key4 message 3', description: 'key4 desc'},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('localizePluginTranslationFile', () => {
|
||||
test('not localize if localized file does not exist', async () => {
|
||||
const siteDir = await createTmpSiteDir();
|
||||
|
||||
const translationFile: TranslationFile = {
|
||||
path: 'my/translation/file',
|
||||
content: {
|
||||
key1: {message: 'key1 message'},
|
||||
key2: {message: 'key2 message'},
|
||||
key3: {message: 'key3 message'},
|
||||
},
|
||||
};
|
||||
|
||||
const localizedTranslationFile = await localizePluginTranslationFile({
|
||||
siteDir,
|
||||
locale: 'fr',
|
||||
translationFile,
|
||||
// @ts-expect-error: enough for this test
|
||||
plugin: {
|
||||
name: 'my-plugin-name',
|
||||
options: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(localizedTranslationFile).toEqual(translationFile);
|
||||
});
|
||||
|
||||
test('not localize if localized file does not exist', async () => {
|
||||
const siteDir = await createTmpSiteDir();
|
||||
|
||||
await writeTranslationFileContent({
|
||||
filePath: path.join(
|
||||
siteDir,
|
||||
'i18n',
|
||||
'fr',
|
||||
'my-plugin-name',
|
||||
'my/translation/file.json',
|
||||
),
|
||||
content: {
|
||||
key2: {message: 'key2 message localized'},
|
||||
key4: {message: 'key4 message localized'},
|
||||
},
|
||||
});
|
||||
|
||||
const translationFile: TranslationFile = {
|
||||
path: 'my/translation/file',
|
||||
content: {
|
||||
key1: {message: 'key1 message'},
|
||||
key2: {message: 'key2 message'},
|
||||
key3: {message: 'key3 message'},
|
||||
},
|
||||
};
|
||||
|
||||
const localizedTranslationFile = await localizePluginTranslationFile({
|
||||
siteDir,
|
||||
locale: 'fr',
|
||||
translationFile,
|
||||
// @ts-expect-error: enough for this test
|
||||
plugin: {
|
||||
name: 'my-plugin-name',
|
||||
options: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(localizedTranslationFile).toEqual({
|
||||
path: translationFile.path,
|
||||
content: {
|
||||
// We only append/override localized messages, but never delete the data of the unlocalized translation file
|
||||
// This ensures that all required keys are present when trying to read the translations files
|
||||
key1: {message: 'key1 message'},
|
||||
key2: {message: 'key2 message localized'},
|
||||
key3: {message: 'key3 message'},
|
||||
key4: {message: 'key4 message localized'},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,296 @@
|
|||
/**
|
||||
* 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 fs from 'fs-extra';
|
||||
import tmp from 'tmp-promise';
|
||||
import {
|
||||
extractSourceCodeFileTranslations,
|
||||
extractPluginsSourceCodeTranslations,
|
||||
} from '../translationsExtractor';
|
||||
import {getBabelOptions} from '../../../webpack/utils';
|
||||
import path from 'path';
|
||||
import {InitPlugin} from '../../plugins/init';
|
||||
|
||||
const TestBabelOptions = getBabelOptions({
|
||||
isServer: true,
|
||||
});
|
||||
|
||||
async function createTmpDir() {
|
||||
const {path: siteDirPath} = await tmp.dir({
|
||||
prefix: 'jest-createTmpSiteDir',
|
||||
});
|
||||
return siteDirPath;
|
||||
}
|
||||
|
||||
async function createTmpSourceCodeFile({
|
||||
extension,
|
||||
content,
|
||||
}: {
|
||||
extension: string;
|
||||
content: string;
|
||||
}) {
|
||||
const file = await tmp.file({
|
||||
prefix: 'jest-createTmpSourceCodeFile',
|
||||
postfix: `.${extension}`,
|
||||
});
|
||||
|
||||
await fs.writeFile(file.path, content);
|
||||
|
||||
return {
|
||||
sourceCodeFilePath: file.path,
|
||||
};
|
||||
}
|
||||
|
||||
describe('extractSourceCodeTranslations', () => {
|
||||
test('throw for bad source code', async () => {
|
||||
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||
extension: 'js',
|
||||
content: `
|
||||
const default => {
|
||||
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
await expect(
|
||||
extractSourceCodeFileTranslations(sourceCodeFilePath, TestBabelOptions),
|
||||
).rejects.toThrowError(
|
||||
/Error while attempting to extract Docusaurus translations from source code file at path/,
|
||||
);
|
||||
});
|
||||
|
||||
test('extract nothing from untranslated source code', async () => {
|
||||
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||
extension: 'js',
|
||||
content: `
|
||||
const unrelated = 42;
|
||||
`,
|
||||
});
|
||||
|
||||
const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
|
||||
sourceCodeFilePath,
|
||||
TestBabelOptions,
|
||||
);
|
||||
|
||||
expect(sourceCodeFileTranslations).toEqual({
|
||||
sourceCodeFilePath,
|
||||
translations: {},
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('extract from a translate() function call', async () => {
|
||||
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||
extension: 'js',
|
||||
content: `
|
||||
export default function MyComponent() {
|
||||
return (
|
||||
<div>
|
||||
<input text={translate({id: 'codeId',message: 'code message',description: 'code description'})}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
|
||||
sourceCodeFilePath,
|
||||
TestBabelOptions,
|
||||
);
|
||||
|
||||
expect(sourceCodeFileTranslations).toEqual({
|
||||
sourceCodeFilePath,
|
||||
translations: {
|
||||
codeId: {message: 'code message', description: 'code description'},
|
||||
},
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('extract from a <Translate> component', async () => {
|
||||
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||
extension: 'js',
|
||||
content: `
|
||||
export default function MyComponent() {
|
||||
return (
|
||||
<div>
|
||||
<Translate id="codeId" description={"code description"}>
|
||||
code message
|
||||
</Translate>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
|
||||
sourceCodeFilePath,
|
||||
TestBabelOptions,
|
||||
);
|
||||
|
||||
expect(sourceCodeFileTranslations).toEqual({
|
||||
sourceCodeFilePath,
|
||||
translations: {
|
||||
codeId: {message: 'code message', description: 'code description'},
|
||||
},
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('extract statically evaluable content', async () => {
|
||||
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||
extension: 'js',
|
||||
content: `
|
||||
const prefix = "prefix ";
|
||||
|
||||
export default function MyComponent() {
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
text={translate({
|
||||
id: prefix + 'codeId fn',
|
||||
message: prefix + 'code message',
|
||||
description: prefix + 'code description'}
|
||||
)}
|
||||
/>
|
||||
<Translate
|
||||
id={prefix + "codeId comp"}
|
||||
description={prefix + "code description"}
|
||||
>{prefix + "code message"}</Translate>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
|
||||
sourceCodeFilePath,
|
||||
TestBabelOptions,
|
||||
);
|
||||
|
||||
expect(sourceCodeFileTranslations).toEqual({
|
||||
sourceCodeFilePath,
|
||||
translations: {
|
||||
'prefix codeId comp': {
|
||||
message: 'prefix code message',
|
||||
description: 'prefix code description',
|
||||
},
|
||||
'prefix codeId fn': {
|
||||
message: 'prefix code message',
|
||||
description: 'prefix code description',
|
||||
},
|
||||
},
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('extract from TypeScript file', async () => {
|
||||
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
|
||||
extension: 'tsx',
|
||||
content: `
|
||||
type ComponentProps<T> = {toto: string}
|
||||
|
||||
export default function MyComponent<T>(props: ComponentProps<T>) {
|
||||
return (
|
||||
<div>
|
||||
<input text={translate({id: 'codeId',message: 'code message',description: 'code description'}) as string}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
|
||||
sourceCodeFilePath,
|
||||
TestBabelOptions,
|
||||
);
|
||||
|
||||
expect(sourceCodeFileTranslations).toEqual({
|
||||
sourceCodeFilePath,
|
||||
translations: {
|
||||
codeId: {message: 'code message', description: 'code description'},
|
||||
},
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractPluginsSourceCodeTranslations', () => {
|
||||
test('should extract translation from all plugins source code', async () => {
|
||||
function createTestPlugin(pluginDir: string): InitPlugin {
|
||||
// @ts-expect-error: good enough for this test
|
||||
return {
|
||||
name: 'abc',
|
||||
getPathsToWatch() {
|
||||
return [path.join(pluginDir, '**/*.{js,jsx,ts,tsx}')];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const plugin1Dir = await createTmpDir();
|
||||
const plugin1File = path.join(plugin1Dir, 'file.jsx');
|
||||
await fs.ensureDir(path.dirname(plugin1File));
|
||||
await fs.writeFile(
|
||||
plugin1File,
|
||||
`
|
||||
export default function MyComponent() {
|
||||
return (
|
||||
<div>
|
||||
<input text={translate({id: 'plugin1Id',message: 'plugin1 message',description: 'plugin1 description'})}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`,
|
||||
);
|
||||
const plugin1 = createTestPlugin(plugin1Dir);
|
||||
|
||||
const plugin2Dir = await createTmpDir();
|
||||
const plugin2File = path.join(plugin1Dir, 'sub', 'path', 'file.tsx');
|
||||
await fs.ensureDir(path.dirname(plugin2File));
|
||||
await fs.writeFile(
|
||||
plugin2File,
|
||||
`
|
||||
type Props = {hey: string};
|
||||
|
||||
export default function MyComponent(props: Props) {
|
||||
return (
|
||||
<div>
|
||||
<input text={translate({id: 'plugin2Id',message: 'plugin2 message',description: 'plugin2 description'})}/>
|
||||
<Translate
|
||||
id="plugin2Id2"
|
||||
description="plugin2 description 2"
|
||||
>
|
||||
plugin2 message 2
|
||||
</Translate>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`,
|
||||
);
|
||||
const plugin2 = createTestPlugin(plugin2Dir);
|
||||
|
||||
const plugins = [plugin1, plugin2];
|
||||
const translations = await extractPluginsSourceCodeTranslations(
|
||||
plugins,
|
||||
TestBabelOptions,
|
||||
);
|
||||
expect(translations).toEqual({
|
||||
plugin1Id: {
|
||||
description: 'plugin1 description',
|
||||
message: 'plugin1 message',
|
||||
},
|
||||
plugin2Id: {
|
||||
description: 'plugin2 description',
|
||||
message: 'plugin2 message',
|
||||
},
|
||||
plugin2Id2: {
|
||||
description: 'plugin2 description 2',
|
||||
message: 'plugin2 message 2',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
259
packages/docusaurus/src/server/translations/translations.ts
Normal file
259
packages/docusaurus/src/server/translations/translations.ts
Normal file
|
@ -0,0 +1,259 @@
|
|||
/**
|
||||
* 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 path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import {InitPlugin} from '../plugins/init';
|
||||
import {mapValues, difference} from 'lodash';
|
||||
import {TranslationFileContent, TranslationFile} from '@docusaurus/types';
|
||||
import {getPluginI18nPath} from '@docusaurus/utils';
|
||||
import * as Joi from 'joi';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export type WriteTranslationsOptions = {
|
||||
override?: boolean;
|
||||
messagePrefix?: string;
|
||||
};
|
||||
|
||||
type TranslationContext = {
|
||||
siteDir: string;
|
||||
locale: string;
|
||||
};
|
||||
|
||||
const TranslationFileContentSchema = Joi.object<TranslationFileContent>()
|
||||
.pattern(
|
||||
Joi.string(),
|
||||
Joi.object({
|
||||
message: Joi.string().allow('').required(),
|
||||
description: Joi.string().optional(),
|
||||
}),
|
||||
)
|
||||
.required();
|
||||
|
||||
export function ensureTranslationFileContent(
|
||||
content: unknown,
|
||||
): asserts content is TranslationFileContent {
|
||||
Joi.attempt(content, TranslationFileContentSchema, {
|
||||
abortEarly: false,
|
||||
allowUnknown: false,
|
||||
convert: false,
|
||||
});
|
||||
}
|
||||
|
||||
export async function readTranslationFileContent(
|
||||
filePath: string,
|
||||
): Promise<TranslationFileContent | undefined> {
|
||||
if (await fs.pathExists(filePath)) {
|
||||
try {
|
||||
const content = JSON.parse(await fs.readFile(filePath, 'utf8'));
|
||||
ensureTranslationFileContent(content);
|
||||
return content;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Invalid translation file at path=${filePath}.\n${e.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function mergeTranslationFileContent({
|
||||
existingContent = {},
|
||||
newContent,
|
||||
options,
|
||||
}: {
|
||||
existingContent: TranslationFileContent | undefined;
|
||||
newContent: TranslationFileContent;
|
||||
options: WriteTranslationsOptions;
|
||||
}): TranslationFileContent {
|
||||
// Apply messagePrefix to all messages
|
||||
const newContentTransformed = mapValues(newContent, (value) => ({
|
||||
...value,
|
||||
message: `${options.messagePrefix ?? ''}${value.message}`,
|
||||
}));
|
||||
|
||||
const result: TranslationFileContent = {...existingContent};
|
||||
|
||||
// We only add missing keys here, we don't delete existing ones
|
||||
Object.entries(newContentTransformed).forEach(
|
||||
([key, {message, description}]) => {
|
||||
result[key] = {
|
||||
// If the messages already exist, we don't override them (unless requested)
|
||||
message: options.override
|
||||
? message
|
||||
: existingContent[key]?.message ?? message,
|
||||
description, // description
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function writeTranslationFileContent({
|
||||
filePath,
|
||||
content: newContent,
|
||||
options = {},
|
||||
}: {
|
||||
filePath: string;
|
||||
content: TranslationFileContent;
|
||||
options?: WriteTranslationsOptions;
|
||||
}): Promise<void> {
|
||||
const existingContent = await readTranslationFileContent(filePath);
|
||||
|
||||
// Warn about potential legacy keys
|
||||
const unknownKeys = difference(
|
||||
Object.keys(existingContent ?? {}),
|
||||
Object.keys(newContent),
|
||||
);
|
||||
if (unknownKeys.length > 0) {
|
||||
console.warn(
|
||||
chalk.yellow(`Some translation keys looks unknown to us in file ${filePath}
|
||||
Maybe you should remove them?
|
||||
- ${unknownKeys.join('\n- ')}`),
|
||||
);
|
||||
}
|
||||
|
||||
const mergedContent = mergeTranslationFileContent({
|
||||
existingContent,
|
||||
newContent,
|
||||
options,
|
||||
});
|
||||
|
||||
// Avoid creating empty translation files
|
||||
if (Object.keys(mergedContent).length > 0) {
|
||||
console.log(
|
||||
`${Object.keys(mergedContent)
|
||||
.length.toString()
|
||||
.padStart(3, ' ')} translations written at ${path.relative(
|
||||
process.cwd(),
|
||||
filePath,
|
||||
)}`,
|
||||
);
|
||||
await fs.ensureDir(path.dirname(filePath));
|
||||
await fs.writeFile(filePath, JSON.stringify(mergedContent, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
// should we make this configurable?
|
||||
export function getTranslationsDirPath(context: TranslationContext): string {
|
||||
return path.resolve(path.join(context.siteDir, `i18n`));
|
||||
}
|
||||
export function getTranslationsLocaleDirPath(
|
||||
context: TranslationContext,
|
||||
): string {
|
||||
return path.join(getTranslationsDirPath(context), context.locale);
|
||||
}
|
||||
|
||||
export function getCodeTranslationsFilePath(
|
||||
context: TranslationContext,
|
||||
): string {
|
||||
return path.join(getTranslationsLocaleDirPath(context), 'code.json');
|
||||
}
|
||||
|
||||
export async function readCodeTranslationFileContent(
|
||||
context: TranslationContext,
|
||||
): Promise<TranslationFileContent | undefined> {
|
||||
return readTranslationFileContent(getCodeTranslationsFilePath(context));
|
||||
}
|
||||
export async function writeCodeTranslations(
|
||||
context: TranslationContext,
|
||||
content: TranslationFileContent,
|
||||
options: WriteTranslationsOptions,
|
||||
): Promise<void> {
|
||||
return writeTranslationFileContent({
|
||||
filePath: getCodeTranslationsFilePath(context),
|
||||
content,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
// We ask users to not provide any extension on purpose:
|
||||
// maybe some day we'll want to support multiple FS formats?
|
||||
// (json/yaml/toml/xml...)
|
||||
function addTranslationFileExtension(translationFilePath: string) {
|
||||
if (translationFilePath.endsWith('.json')) {
|
||||
throw new Error(
|
||||
`Translation file path does not need to end with .json, we addt the extension automatically. translationFilePath=${translationFilePath}`,
|
||||
);
|
||||
}
|
||||
return `${translationFilePath}.json`;
|
||||
}
|
||||
|
||||
function getPluginTranslationFilePath({
|
||||
siteDir,
|
||||
plugin,
|
||||
locale,
|
||||
translationFilePath,
|
||||
}: TranslationContext & {
|
||||
plugin: InitPlugin;
|
||||
translationFilePath: string;
|
||||
}): string {
|
||||
const dirPath = getPluginI18nPath({
|
||||
siteDir,
|
||||
locale,
|
||||
pluginName: plugin.name,
|
||||
pluginId: plugin.options.id,
|
||||
});
|
||||
const filePath = addTranslationFileExtension(translationFilePath);
|
||||
return path.join(dirPath, filePath);
|
||||
}
|
||||
|
||||
export async function writePluginTranslations({
|
||||
siteDir,
|
||||
plugin,
|
||||
locale,
|
||||
translationFile,
|
||||
options,
|
||||
}: TranslationContext & {
|
||||
plugin: InitPlugin;
|
||||
translationFile: TranslationFile;
|
||||
options?: WriteTranslationsOptions;
|
||||
}): Promise<void> {
|
||||
const filePath = getPluginTranslationFilePath({
|
||||
plugin,
|
||||
siteDir,
|
||||
locale,
|
||||
translationFilePath: translationFile.path,
|
||||
});
|
||||
await writeTranslationFileContent({
|
||||
filePath,
|
||||
content: translationFile.content,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
export async function localizePluginTranslationFile({
|
||||
siteDir,
|
||||
plugin,
|
||||
locale,
|
||||
translationFile,
|
||||
}: TranslationContext & {
|
||||
plugin: InitPlugin;
|
||||
translationFile: TranslationFile;
|
||||
}): Promise<TranslationFile> {
|
||||
const filePath = getPluginTranslationFilePath({
|
||||
plugin,
|
||||
siteDir,
|
||||
locale,
|
||||
translationFilePath: translationFile.path,
|
||||
});
|
||||
|
||||
const localizedContent = await readTranslationFileContent(filePath);
|
||||
|
||||
if (localizedContent) {
|
||||
// localized messages "override" default unlocalized messages
|
||||
return {
|
||||
path: translationFile.path,
|
||||
content: {
|
||||
...translationFile.content,
|
||||
...localizedContent,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return translationFile;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,276 @@
|
|||
/**
|
||||
* 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 fs from 'fs-extra';
|
||||
import traverse, {Node} from '@babel/traverse';
|
||||
import generate from '@babel/generator';
|
||||
import chalk from 'chalk';
|
||||
import {parse, types as t, NodePath, TransformOptions} from '@babel/core';
|
||||
import {flatten} from 'lodash';
|
||||
import {TranslationFileContent, TranslationMessage} from '@docusaurus/types';
|
||||
import globby from 'globby';
|
||||
import nodePath from 'path';
|
||||
import {InitPlugin} from '../plugins/init';
|
||||
|
||||
// We only support extracting source code translations from these kind of files
|
||||
const TranslatableSourceCodeExtension = new Set([
|
||||
'.js',
|
||||
'.jsx',
|
||||
'.ts',
|
||||
'.tsx',
|
||||
// TODO support md/mdx too? (may be overkill)
|
||||
// need to compile the MDX to JSX first and remove frontmatter
|
||||
// '.md',
|
||||
// '.mdx',
|
||||
]);
|
||||
function isTranslatableSourceCodePath(filePath: string): boolean {
|
||||
return TranslatableSourceCodeExtension.has(nodePath.extname(filePath));
|
||||
}
|
||||
|
||||
async function getSourceCodeFilePaths(
|
||||
plugins: InitPlugin[],
|
||||
): Promise<string[]> {
|
||||
// The getPathsToWatch() generally returns the js/jsx/ts/tsx/md/mdx file paths
|
||||
// We can use this method as well to know which folders we should try to extract translations from
|
||||
// Hacky/implicit, but do we want to introduce a new lifecycle method for that???
|
||||
const allPathsToWatch = flatten(
|
||||
plugins.map((plugin) => plugin.getPathsToWatch?.() ?? []),
|
||||
);
|
||||
|
||||
const filePaths = await globby(allPathsToWatch);
|
||||
|
||||
return filePaths.filter(isTranslatableSourceCodePath);
|
||||
}
|
||||
|
||||
export async function extractPluginsSourceCodeTranslations(
|
||||
plugins: InitPlugin[],
|
||||
babelOptions: TransformOptions,
|
||||
): Promise<TranslationFileContent> {
|
||||
// Should we warn here if the same translation "key" is found in multiple source code files?
|
||||
function toTranslationFileContent(
|
||||
sourceCodeFileTranslations: SourceCodeFileTranslations[],
|
||||
): TranslationFileContent {
|
||||
return sourceCodeFileTranslations.reduce((acc, item) => {
|
||||
return {...acc, ...item.translations};
|
||||
}, {});
|
||||
}
|
||||
|
||||
const sourceCodeFilePaths = await getSourceCodeFilePaths(plugins);
|
||||
const sourceCodeFilesTranslations = await extractAllSourceCodeFileTranslations(
|
||||
sourceCodeFilePaths,
|
||||
babelOptions,
|
||||
);
|
||||
|
||||
logSourceCodeFileTranslationsWarnings(sourceCodeFilesTranslations);
|
||||
|
||||
return toTranslationFileContent(sourceCodeFilesTranslations);
|
||||
}
|
||||
|
||||
function logSourceCodeFileTranslationsWarnings(
|
||||
sourceCodeFilesTranslations: SourceCodeFileTranslations[],
|
||||
) {
|
||||
sourceCodeFilesTranslations.forEach(({sourceCodeFilePath, warnings}) => {
|
||||
if (warnings.length > 0) {
|
||||
console.warn(
|
||||
`Translation extraction warnings for file path=${sourceCodeFilePath}:\n- ${chalk.yellow(
|
||||
warnings.join('\n\n- '),
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
type SourceCodeFileTranslations = {
|
||||
sourceCodeFilePath: string;
|
||||
translations: Record<string, TranslationMessage>;
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
async function extractAllSourceCodeFileTranslations(
|
||||
sourceCodeFilePaths: string[],
|
||||
babelOptions: TransformOptions,
|
||||
): Promise<SourceCodeFileTranslations[]> {
|
||||
return flatten(
|
||||
await Promise.all(
|
||||
sourceCodeFilePaths.map((sourceFilePath) =>
|
||||
extractSourceCodeFileTranslations(sourceFilePath, babelOptions),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export async function extractSourceCodeFileTranslations(
|
||||
sourceCodeFilePath: string,
|
||||
babelOptions: TransformOptions,
|
||||
): Promise<SourceCodeFileTranslations> {
|
||||
try {
|
||||
const code = await fs.readFile(sourceCodeFilePath, 'utf8');
|
||||
|
||||
const ast = parse(code, {
|
||||
...babelOptions,
|
||||
ast: true,
|
||||
// filename is important, because babel does not process the same files according to their js/ts extensions
|
||||
// see see https://twitter.com/NicoloRibaudo/status/1321130735605002243
|
||||
filename: sourceCodeFilePath,
|
||||
}) as Node;
|
||||
|
||||
return await extractSourceCodeAstTranslations(ast, sourceCodeFilePath);
|
||||
} catch (e) {
|
||||
e.message = `Error while attempting to extract Docusaurus translations from source code file at path=${sourceCodeFilePath}\n${e.message}`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Need help understanding this?
|
||||
|
||||
Useful resources:
|
||||
https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md
|
||||
https://github.com/formatjs/formatjs/blob/main/packages/babel-plugin-react-intl/index.ts
|
||||
https://github.com/pugjs/babel-walk
|
||||
*/
|
||||
function extractSourceCodeAstTranslations(
|
||||
ast: Node,
|
||||
sourceCodeFilePath: string,
|
||||
): SourceCodeFileTranslations {
|
||||
function staticTranslateJSXWarningPart() {
|
||||
return 'Translate content could not be extracted.\nIt has to be a static string, like <Translate>text</Translate>.';
|
||||
}
|
||||
function sourceFileWarningPart(node: Node) {
|
||||
return `File=${sourceCodeFilePath} at line=${node.loc?.start.line}`;
|
||||
}
|
||||
function generateCode(node: Node) {
|
||||
return generate(node as any).code;
|
||||
}
|
||||
|
||||
const translations: Record<string, TranslationMessage> = {};
|
||||
const warnings: string[] = [];
|
||||
|
||||
// TODO we should check the presence of the correct @docusaurus imports here!
|
||||
|
||||
traverse(ast, {
|
||||
JSXElement(path) {
|
||||
function evaluateJSXProp(propName: string): string | undefined {
|
||||
const attributePath = path
|
||||
.get('openingElement.attributes')
|
||||
.find(
|
||||
(attr) => attr.isJSXAttribute() && attr.node.name.name === propName,
|
||||
);
|
||||
|
||||
if (attributePath) {
|
||||
const attributeValue = attributePath.get('value') as NodePath;
|
||||
|
||||
const attributeValueEvaluated =
|
||||
attributeValue.node.type === 'JSXExpressionContainer'
|
||||
? (attributeValue.get('expression') as NodePath).evaluate()
|
||||
: attributeValue.evaluate();
|
||||
|
||||
if (
|
||||
attributeValueEvaluated.confident &&
|
||||
typeof attributeValueEvaluated.value === 'string'
|
||||
) {
|
||||
return attributeValueEvaluated.value;
|
||||
} else {
|
||||
warnings.push(
|
||||
`<Translate> prop=${propName} should be a statically evaluable object.\nExample: <Translate id="optional.id" description="optional description">Message</Translate>\nDynamically constructed values are not allowed, because they prevent translations to be extracted.\n${sourceFileWarningPart(
|
||||
path.node,
|
||||
)}\n${generateCode(path.node)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
path.node.openingElement.name.type === 'JSXIdentifier' &&
|
||||
path.node.openingElement.name.name === 'Translate'
|
||||
) {
|
||||
// TODO support multiple childrens here?
|
||||
if (
|
||||
path.node.children.length === 1 &&
|
||||
t.isJSXText(path.node.children[0])
|
||||
) {
|
||||
const message = path.node.children[0].value
|
||||
.trim()
|
||||
.replace(/\s+/g, ' ');
|
||||
|
||||
const id = evaluateJSXProp('id');
|
||||
const description = evaluateJSXProp('description');
|
||||
|
||||
translations[id ?? message] = {
|
||||
message,
|
||||
...(description && {description}),
|
||||
};
|
||||
} else if (
|
||||
path.node.children.length === 1 &&
|
||||
t.isJSXExpressionContainer(path.node.children[0]) &&
|
||||
(path.get('children.0.expression') as NodePath).evaluate().confident
|
||||
) {
|
||||
const message = (path.get(
|
||||
'children.0.expression',
|
||||
) as NodePath).evaluate().value;
|
||||
|
||||
const id = evaluateJSXProp('id');
|
||||
const description = evaluateJSXProp('description');
|
||||
|
||||
translations[id ?? message] = {
|
||||
message,
|
||||
...(description && {description}),
|
||||
};
|
||||
} else {
|
||||
warnings.push(
|
||||
`${staticTranslateJSXWarningPart}\n${sourceFileWarningPart(
|
||||
path.node,
|
||||
)}\n${generateCode(path.node)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
CallExpression(path) {
|
||||
if (
|
||||
path.node.callee.type === 'Identifier' &&
|
||||
path.node.callee.name === 'translate'
|
||||
) {
|
||||
// console.log('CallExpression', path.node);
|
||||
if (path.node.arguments.length === 1) {
|
||||
const firstArgPath = path.get('arguments.0') as NodePath;
|
||||
|
||||
// evaluation allows translate("x" + "y"); to be considered as translate("xy");
|
||||
const firstArgEvaluated = firstArgPath.evaluate();
|
||||
|
||||
// console.log('firstArgEvaluated', firstArgEvaluated);
|
||||
|
||||
if (
|
||||
firstArgEvaluated.confident &&
|
||||
typeof firstArgEvaluated.value === 'object'
|
||||
) {
|
||||
const {message, id, description} = firstArgEvaluated.value;
|
||||
translations[id ?? message] = {
|
||||
message,
|
||||
...(description && {description}),
|
||||
};
|
||||
} else {
|
||||
warnings.push(
|
||||
`translate() first arg should be a statically evaluable object.\nExample: translate({message: "text",id: "optional.id",description: "optional description"}\nDynamically constructed values are not allowed, because they prevent translations to be extracted.\n${sourceFileWarningPart(
|
||||
path.node,
|
||||
)}\n${generateCode(path.node)}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warnings.push(
|
||||
`translate() function only takes 1 arg\n${sourceFileWarningPart(
|
||||
path.node,
|
||||
)}\n${generateCode(path.node)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {sourceCodeFilePath, translations, warnings};
|
||||
}
|
|
@ -8,6 +8,7 @@ Object {
|
|||
"@docusaurus/Head": "../../client/exports/Head.tsx",
|
||||
"@docusaurus/Link": "../../client/exports/Link.tsx",
|
||||
"@docusaurus/Noop": "../../client/exports/Noop.ts",
|
||||
"@docusaurus/Translate": "../../client/exports/Translate.tsx",
|
||||
"@docusaurus/constants": "../../client/exports/constants.ts",
|
||||
"@docusaurus/context": "../../client/exports/context.ts",
|
||||
"@docusaurus/isInternalUrl": "../../client/exports/isInternalUrl.ts",
|
||||
|
|
|
@ -16,9 +16,9 @@ import {
|
|||
getCacheLoader,
|
||||
getStyleLoaders,
|
||||
getFileLoaderUtils,
|
||||
getCustomBabelConfigFilePath,
|
||||
getMinimizer,
|
||||
} from './utils';
|
||||
import {BABEL_CONFIG_FILE_NAME} from '../constants';
|
||||
|
||||
const CSS_REGEX = /\.css$/;
|
||||
const CSS_MODULE_REGEX = /\.module\.css$/;
|
||||
|
@ -68,11 +68,6 @@ export function createBaseConfig(
|
|||
const minimizeEnabled = minify && isProd && !isServer;
|
||||
const useSimpleCssMinifier = process.env.USE_SIMPLE_CSS_MINIFIER === 'true';
|
||||
|
||||
const customBabelConfigurationPath = path.join(
|
||||
siteDir,
|
||||
BABEL_CONFIG_FILE_NAME,
|
||||
);
|
||||
|
||||
const fileLoaderUtils = getFileLoaderUtils();
|
||||
|
||||
return {
|
||||
|
@ -162,12 +157,7 @@ export function createBaseConfig(
|
|||
exclude: excludeJS,
|
||||
use: [
|
||||
getCacheLoader(isServer),
|
||||
getBabelLoader(
|
||||
isServer,
|
||||
fs.existsSync(customBabelConfigurationPath)
|
||||
? customBabelConfigurationPath
|
||||
: undefined,
|
||||
),
|
||||
getBabelLoader(isServer, getCustomBabelConfigFilePath(siteDir)),
|
||||
].filter(Boolean) as Loader[],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -9,10 +9,10 @@ import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
|||
import env from 'std-env';
|
||||
import merge from 'webpack-merge';
|
||||
import webpack, {Configuration, Loader, RuleSetRule, Stats} from 'webpack';
|
||||
import fs from 'fs-extra';
|
||||
import TerserPlugin from 'terser-webpack-plugin';
|
||||
import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin';
|
||||
import CleanCss from 'clean-css';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
import chalk from 'chalk';
|
||||
|
@ -20,7 +20,7 @@ import {TransformOptions} from '@babel/core';
|
|||
import {ConfigureWebpackFn} from '@docusaurus/types';
|
||||
import CssNanoPreset from '@docusaurus/cssnano-preset';
|
||||
import {version as cacheLoaderVersion} from 'cache-loader/package.json';
|
||||
import {STATIC_ASSETS_DIR_NAME} from '../constants';
|
||||
import {BABEL_CONFIG_FILE_NAME, STATIC_ASSETS_DIR_NAME} from '../constants';
|
||||
|
||||
// Utility method to get style loaders
|
||||
export function getStyleLoaders(
|
||||
|
@ -93,19 +93,33 @@ export function getCacheLoader(
|
|||
};
|
||||
}
|
||||
|
||||
export function getBabelLoader(
|
||||
isServer: boolean,
|
||||
babelOptions?: TransformOptions | string,
|
||||
): Loader {
|
||||
let options: TransformOptions;
|
||||
export function getCustomBabelConfigFilePath(
|
||||
siteDir: string,
|
||||
): string | undefined {
|
||||
const customBabelConfigurationPath = path.join(
|
||||
siteDir,
|
||||
BABEL_CONFIG_FILE_NAME,
|
||||
);
|
||||
return fs.existsSync(customBabelConfigurationPath)
|
||||
? customBabelConfigurationPath
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function getBabelOptions({
|
||||
isServer,
|
||||
babelOptions,
|
||||
}: {
|
||||
isServer?: boolean;
|
||||
babelOptions?: TransformOptions | string;
|
||||
} = {}): TransformOptions {
|
||||
if (typeof babelOptions === 'string') {
|
||||
options = {
|
||||
return {
|
||||
babelrc: false,
|
||||
configFile: babelOptions,
|
||||
caller: {name: isServer ? 'server' : 'client'},
|
||||
};
|
||||
} else {
|
||||
options = Object.assign(
|
||||
return Object.assign(
|
||||
babelOptions ?? {presets: [require.resolve('../babel/preset')]},
|
||||
{
|
||||
babelrc: false,
|
||||
|
@ -114,9 +128,15 @@ export function getBabelLoader(
|
|||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getBabelLoader(
|
||||
isServer: boolean,
|
||||
babelOptions?: TransformOptions | string,
|
||||
): Loader {
|
||||
return {
|
||||
loader: require.resolve('babel-loader'),
|
||||
options,
|
||||
options: getBabelOptions({isServer, babelOptions}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
"alwaysStrict": true,
|
||||
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedLocals": false, // ensured by eslint, should not block compilation
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
|
|
|
@ -49,8 +49,12 @@ module.exports = {
|
|||
baseUrl,
|
||||
baseUrlIssueBanner: true,
|
||||
url: 'https://v2.docusaurus.io',
|
||||
onBrokenLinks: isVersioningDisabled ? 'warn' : 'throw',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'fr'],
|
||||
},
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenMarkdownLinks: 'throw',
|
||||
favicon: 'img/docusaurus.ico',
|
||||
customFields: {
|
||||
description:
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue