diff --git a/crowdin.yaml b/crowdin.yaml index 1ebe5fbde688..0e9df3eb53dc 100644 --- a/crowdin.yaml +++ b/crowdin.yaml @@ -5,8 +5,8 @@ preserve_hierarchy: true files: - - source: '/docs/*.md' - translation: '/website/translated_docs/%locale%/%original_file_name%' + source: '/docs/**/*.md' + translation: '/website/translated_docs/%locale%/**/%original_file_name%' languages_mapping: &anchor locale: 'af': 'af' diff --git a/docs/guides-translation.md b/docs/guides-translation.md index 3624693fd4af..d9ad2c997057 100644 --- a/docs/guides-translation.md +++ b/docs/guides-translation.md @@ -140,8 +140,8 @@ preserve_hierarchy: true files: - - source: '/docs/*.md' - translation: '/website/translated_docs/%locale%/%original_file_name%' + source: '/docs/**/*.md' + translation: '/website/translated_docs/%locale%/**/%original_file_name%' languages_mapping: &anchor locale: 'de': 'de' diff --git a/lib/__tests__/build-files.test.js b/lib/__tests__/build-files.test.js index fe92052e7b54..6953eddc8db4 100644 --- a/lib/__tests__/build-files.test.js +++ b/lib/__tests__/build-files.test.js @@ -42,7 +42,7 @@ beforeAll(() => { generateSite(); return Promise.all([ glob(docsDir + '/**/*.md'), - glob(buildDir + '/' + siteConfig.projectName + '/docs/*.html'), + glob(buildDir + '/' + siteConfig.projectName + '/docs/**/*.html'), glob(docsDir + '/assets/*'), glob(buildDir + '/' + siteConfig.projectName + '/img/*'), ]).then(function(results) { diff --git a/lib/core/DocsLayout.js b/lib/core/DocsLayout.js index c1faf4a2db65..6314d3ba4426 100644 --- a/lib/core/DocsLayout.js +++ b/lib/core/DocsLayout.js @@ -12,9 +12,20 @@ const DocsSidebar = require('./DocsSidebar.js'); const OnPageNav = require('./nav/OnPageNav.js'); const Site = require('./Site.js'); const translation = require('../server/translation.js'); +const path = require('path'); // component used to generate whole webpage for docs, including sidebar/header/footer class DocsLayout extends React.Component { + getRelativeURL = (from, to) => { + const extension = this.props.config.cleanUrl ? '' : '.html'; + return ( + path + .relative(from, to) + .replace('\\', '/') + .replace(/^\.\.\//, '') + extension + ); + }; + render() { const metadata = this.props.metadata; const content = this.props.children; @@ -28,7 +39,6 @@ class DocsLayout extends React.Component { this.props.metadata.localized_id ] || this.props.metadata.title : this.props.metadata.title; - const extension = this.props.config.cleanUrl ? '' : '.html'; return ( + href={this.getRelativeURL( + metadata.localized_id, + metadata.previous_id + )}> ←{' '} {i18n ? translation[this.props.metadata.language][ @@ -71,7 +84,10 @@ class DocsLayout extends React.Component { {metadata.next_id && ( + href={this.getRelativeURL( + metadata.localized_id, + metadata.next_id + )}> {i18n ? translation[this.props.metadata.language][ 'localized-strings' diff --git a/lib/server/readMetadata.js b/lib/server/readMetadata.js index fd1e24551fc8..0d88afbfd7e0 100644 --- a/lib/server/readMetadata.js +++ b/lib/server/readMetadata.js @@ -15,7 +15,7 @@ const chalk = require('chalk'); const env = require('./env.js'); const siteConfig = require(CWD + '/siteConfig.js'); const versionFallback = require('./versionFallback.js'); -const escapeStringRegexp = require('escape-string-regexp'); +const utils = require('./utils.js'); const SupportedHeaderFields = new Set([ 'id', @@ -121,17 +121,10 @@ function extractMetadata(content) { return {metadata, rawContent: both.content}; } -// process the metadata for a document found in the docs folder -function processMetadata(file) { +// process the metadata for a document found in either 'docs' or 'translated_docs' +function processMetadata(file, refDir) { const result = extractMetadata(fs.readFileSync(file, 'utf8')); - - let regexSubFolder = new RegExp( - '/' + escapeStringRegexp(getDocsPath()) + '/(.*)/.*/' - ); - - const match = regexSubFolder.exec(file); - let language = match ? match[1] : 'en'; - + const language = utils.getLanguage(file, refDir) || 'en'; const metadata = {}; for (const fieldName of Object.keys(result.metadata)) { if (SupportedHeaderFields.has(fieldName)) { @@ -142,7 +135,6 @@ function processMetadata(file) { } const rawContent = result.rawContent; - metadata.source = path.basename(file); if (!metadata.id) { metadata.id = path.basename(file, path.extname(file)); @@ -150,6 +142,21 @@ function processMetadata(file) { if (metadata.id.includes('/')) { throw new Error('Document id cannot include "/".'); } + + // If a file is located in a subdirectory, prepend the subdir to it's ID + // Example: + // (file: 'docusaurus/docs/projectA/test.md', ID 'test', refDir: 'docs') + // returns 'projectA/test' + const subDir = utils.getSubDir(file, refDir); + if (subDir) { + metadata.id = `${subDir}/${metadata.id}`; + } + + // Example: `docs/projectA/test.md` source is `projectA/test.md` + metadata.source = subDir + ? `${subDir}/${path.basename(file)}` + : path.basename(file); + if (!metadata.title) { metadata.title = metadata.id; } @@ -209,6 +216,7 @@ function generateMetadataDocs() { const defaultMetadatas = {}; // metadata for english files + const docsDir = path.join(CWD, '../', getDocsPath()); let files = glob.sync(CWD + '/../' + getDocsPath() + '/**'); files.forEach(file => { let language = 'en'; @@ -216,7 +224,7 @@ function generateMetadataDocs() { const extension = path.extname(file); if (extension === '.md' || extension === '.markdown') { - const res = processMetadata(file); + const res = processMetadata(file, docsDir); if (!res) { return; @@ -255,23 +263,17 @@ function generateMetadataDocs() { }); // metadata for non-english docs - const regexSubFolder = /translated_docs\/(.*?)\/.*/; + const translatedDir = path.join(CWD, 'translated_docs'); files = glob.sync(CWD + '/translated_docs/**'); files.forEach(file => { - let language = 'en'; - const match = regexSubFolder.exec(file); - if (match) { - language = match[1]; - } - - if (enabledLanguages.indexOf(language) === -1) { + if (!utils.getLanguage(file, translatedDir)) { return; } const extension = path.extname(file); if (extension === '.md' || extension === '.markdown') { - const res = processMetadata(file); + const res = processMetadata(file, translatedDir); if (!res) { return; } diff --git a/lib/server/server.js b/lib/server/server.js index 4be3d78dca79..e4f6f8db20dc 100644 --- a/lib/server/server.js +++ b/lib/server/server.js @@ -130,7 +130,7 @@ function execute(port) { // handle all requests for document pages const app = express(); - app.get(/docs\/.*html$/, (req, res, next) => { + app.get(/^\/docs\/.*html$/, (req, res, next) => { let url = req.path.toString().replace(siteConfig.baseUrl, ''); // links is a map from a permalink to an id for each document diff --git a/lib/server/utils.js b/lib/server/utils.js new file mode 100644 index 000000000000..843ca27c7568 --- /dev/null +++ b/lib/server/utils.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2017-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +const path = require('path'); +const escapeStringRegexp = require('escape-string-regexp'); +const env = require('./env.js'); + +// Return the subdirectory path from a reference directory +// Example: +// (file: 'docs/projectA/test.md', refDir: 'subDir') +// returns 'projectA' +function getSubDir(file, refDir) { + let subDir = path.dirname(path.relative(refDir, file)); + subDir = subDir.replace('\\', '/'); + return subDir !== '.' ? subDir : null; +} + +// Get the corresponding enabled language locale of a file. +// Example: +// (file: '/website/translated_docs/ko/projectA/test.md', refDir: 'website/translated_docs') +// returns 'ko' +function getLanguage(file, refDir) { + let regexSubFolder = new RegExp( + '/' + escapeStringRegexp(path.basename(refDir)) + '/(.*)/.*/' + ); + const match = regexSubFolder.exec(file); + + // Avoid misinterpreting subdirectory as language + if (match && env.translation.enabled) { + const enabledLanguages = env.translation + .enabledLanguages() + .map(language => language.tag); + if (enabledLanguages.indexOf(match[1]) !== -1) { + return match[1]; + } + } + return null; +} + +module.exports = { + getSubDir, + getLanguage, +}; diff --git a/lib/server/versionFallback.js b/lib/server/versionFallback.js index 099fcb12388d..85c3a9394a2b 100644 --- a/lib/server/versionFallback.js +++ b/lib/server/versionFallback.js @@ -12,6 +12,7 @@ const path = require('path'); const assert = require('assert'); const env = require('./env.js'); +const utils = require('./utils.js'); const siteConfig = require(CWD + '/siteConfig.js'); const ENABLE_TRANSLATION = fs.existsSync(CWD + '/languages.js'); @@ -194,7 +195,25 @@ function diffLatestDoc(file, id) { // the version of the file to be used, and its language function processVersionMetadata(file, version, useVersion, language) { const metadata = extractMetadata(fs.readFileSync(file, 'utf8')).metadata; - metadata.source = 'version-' + useVersion + '/' + path.basename(file); + + // Add subdirectory information to versioned_doc metadata + // Example: `versioned_docs/version-1.1.6/projectA/readme.md` file with id `version-1.1.6-readme` + // and original_id `readme` will have metadata id of `version-1.1.6-projectA/readme` and original_id `projectA/readme` + const subDir = utils.getSubDir( + file, + path.join(CWD, 'versioned_docs', `version-${useVersion}`) + ); + if (subDir) { + metadata.original_id = `${subDir}/${metadata.original_id}`; + metadata.id = metadata.id.replace( + `version-${useVersion}-`, + `version-${useVersion}-${subDir}/` + ); + } + + metadata.source = subDir + ? `version-${useVersion}/${subDir}/${path.basename(file)}` + : `version-${useVersion}/${path.basename(file)}`; const latestVersion = versions[0]; diff --git a/lib/version.js b/lib/version.js index 1d835216b175..0be72f6b4df2 100755 --- a/lib/version.js +++ b/lib/version.js @@ -13,6 +13,7 @@ const path = require('path'); const mkdirp = require('mkdirp'); const chalk = require('chalk'); const readMetadata = require('./server/readMetadata.js'); +const utils = require('./server/utils.js'); const versionFallback = require('./server/versionFallback.js'); const env = require('./server/env.js'); @@ -66,12 +67,18 @@ function makeHeader(metadata) { return header; } +function writeFileAndCreateFolder(file, content, encoding) { + mkdirp.sync(path.dirname(file)); + + fs.writeFileSync(file, content, encoding); +} + const versionFolder = CWD + '/versioned_docs/version-' + version; mkdirp.sync(versionFolder); // copy necessary files to new version, changing some of its metadata to reflect the versioning -let files = glob.sync(CWD + '/../' + readMetadata.getDocsPath() + '/*'); +let files = glob.sync(CWD + '/../' + readMetadata.getDocsPath() + '/**'); files.forEach(file => { const ext = path.extname(file); if (ext !== '.md' && ext !== '.markdown') { @@ -102,9 +109,17 @@ files.forEach(file => { metadata.original_id = metadata.id; metadata.id = 'version-' + version + '-' + metadata.id; - const targetFile = versionFolder + '/' + path.basename(file); + const docsDir = path.join(CWD, '../', readMetadata.getDocsPath()); + const subDir = utils.getSubDir(file, docsDir); + const targetFile = subDir + ? `${versionFolder}/${subDir}/${path.basename(file)}` + : `${versionFolder}/${path.basename(file)}`; - fs.writeFileSync(targetFile, makeHeader(metadata) + rawContent, 'utf8'); + writeFileAndCreateFolder( + targetFile, + makeHeader(metadata) + rawContent, + 'utf8' + ); }); // copy sidebar if necessary diff --git a/lib/write-translations.js b/lib/write-translations.js index 8181313df0d2..4bbfc73b722a 100755 --- a/lib/write-translations.js +++ b/lib/write-translations.js @@ -46,13 +46,14 @@ function execute() { }; // look through markdown headers of docs for titles and categories to translate + const docsDir = path.join(CWD, '../', readMetadata.getDocsPath()); let files = glob.sync(CWD + '/../' + readMetadata.getDocsPath() + '/**'); files.forEach(file => { const extension = path.extname(file); if (extension === '.md' || extension === '.markdown') { let res; try { - res = readMetadata.processMetadata(file); + res = readMetadata.processMetadata(file, docsDir); } catch (e) { console.error(e); process.exit(1);