diff --git a/.github/workflows/update-i18n-deploy.yml b/.github/workflows/update-i18n-deploy.yml new file mode 100644 index 000000000..2f99be9e3 --- /dev/null +++ b/.github/workflows/update-i18n-deploy.yml @@ -0,0 +1,38 @@ +name: 'Update i18n deploy' + +on: + schedule: + - cron: '*/15 * * * *' + +jobs: + deploy: + name: 'Build and deploy localized site' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js 14 + uses: actions/setup-node@v2 + with: + node-version: 14 + + - name: Install dependencies + uses: bahmutov/npm-install@HEAD + + - name: Download crowdin translation + run: yarn i18n:download + env: + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} + + - name: Download cache + run: ./scripts/bin/azcopy copy "https://electronjsorg.blob.core.windows.net/%24web/*?${{ SSA }}" "./build" --recursive + env: + SSA: ${{ secrets.SSA }} + + - name: Build + run: yarn i18n:build + + - name: Deploy + run: ./scripts/bin/azcopy copy "./build/*" "https://electronjsorg.blob.core.windows.net/%24web?${{ SSA }}" --recursive + env: + SAS: ${{ secrets.SSA }} diff --git a/.gitignore b/.gitignore index 36bc9da4e..bbe02d5aa 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ node_modules .env .vscode/settings.json build/ -content/ \ No newline at end of file +content/ +i18n/ +!i18n/en/ \ No newline at end of file diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 000000000..c68c2e83e --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,21 @@ +project_id: '273870' +api_token_env: 'CROWDIN_PERSONAL_TOKEN' +preserve_hierarchy: true +files: [ + # JSON translation files + { + source: '/i18n/en/**/*', + translation: '/i18n/%two_letters_code%/**/%original_file_name%', + }, + # Docs Markdown files + { + source: '/docs/**/*', + translation: '/i18n/%two_letters_code%/docusaurus-plugin-content-docs/current/**/%original_file_name%', + ignore: ['/docs/**/fiddles', '/docs/**/images'], + }, + # Blog Markdown files + { + source: '/blog/**/*', + translation: '/i18n/%two_letters_code%/docusaurus-plugin-content-blog/**/%original_file_name%', + }, + ] diff --git a/docusaurus.config.js b/docusaurus.config.js index 24b60068b..29dc142e1 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -13,6 +13,10 @@ module.exports = { favicon: 'assets/img/favicon.ico', organizationName: 'electron', projectName: 'electron', + i18n: { + defaultLocale: 'en', + locales: ['en', 'de', 'es', 'fr', 'ja', 'pt', 'ru', 'zh'], + }, themeConfig: { announcementBar: { id: 'to_old_docs', @@ -52,6 +56,10 @@ module.exports = { label: 'Releases', position: 'right', }, + { + type: 'localeDropdown', + position: 'right', + }, { href: 'https://github.com/electron/electron', label: 'GitHub', @@ -143,13 +151,17 @@ module.exports = { docs: { sidebarPath: require.resolve('./sidebars.js'), routeBasePath: '/docs/', - editUrl: ({docPath}) => { + editUrl: ({ docPath }) => { // TODO: remove when `latest/` is no longer hardcoded const fixedPath = docPath.replace('latest/', ''); // TODO: versioning? - return `https://github.com/electron/electron/edit/main/docs/${fixedPath}` + return `https://github.com/electron/electron/edit/main/docs/${fixedPath}`; }, - remarkPlugins: [fiddleEmbedder, apiLabels, [npm2yarn, { sync: true }]], + remarkPlugins: [ + fiddleEmbedder, + apiLabels, + [npm2yarn, { sync: true }], + ], }, blog: { // See `node_modules/@docusaurus/plugin-content-blog/src/pluginOptionSchema.ts` for full undocumented options diff --git a/i18n/en-US/code.json b/i18n/en/code.json similarity index 97% rename from i18n/en-US/code.json rename to i18n/en/code.json index 573b647b4..cabc3502d 100644 --- a/i18n/en-US/code.json +++ b/i18n/en/code.json @@ -206,5 +206,13 @@ "theme.tags.tagsPageTitle": { "message": "Tags", "description": "The title of the tag list page" + }, + "theme.blog.archive.title": { + "message": "Archive", + "description": "The page & hero title of the blog archive page" + }, + "theme.blog.archive.description": { + "message": "Archive", + "description": "The page & hero description of the blog archive page" } } \ No newline at end of file diff --git a/i18n/en-US/docusaurus-plugin-content-blog/options.json b/i18n/en/docusaurus-plugin-content-blog/options.json similarity index 100% rename from i18n/en-US/docusaurus-plugin-content-blog/options.json rename to i18n/en/docusaurus-plugin-content-blog/options.json diff --git a/i18n/en-US/docusaurus-plugin-content-docs/current.json b/i18n/en/docusaurus-plugin-content-docs/current.json similarity index 100% rename from i18n/en-US/docusaurus-plugin-content-docs/current.json rename to i18n/en/docusaurus-plugin-content-docs/current.json diff --git a/i18n/en-US/docusaurus-theme-classic/footer.json b/i18n/en/docusaurus-theme-classic/footer.json similarity index 100% rename from i18n/en-US/docusaurus-theme-classic/footer.json rename to i18n/en/docusaurus-theme-classic/footer.json diff --git a/i18n/en-US/docusaurus-theme-classic/navbar.json b/i18n/en/docusaurus-theme-classic/navbar.json similarity index 100% rename from i18n/en-US/docusaurus-theme-classic/navbar.json rename to i18n/en/docusaurus-theme-classic/navbar.json diff --git a/package.json b/package.json index 662818cda..9bd3b78e9 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,13 @@ "private": true, "license": "Apache-2.0", "scripts": { + "crowdin": "crowdin", + "i18n:upload": "crowdin upload sources", + "i18n:download": "crowdin download && node scripts/prepare-i18n-content.js", + "i18n:build": "node scripts/i18n-build.js", "docusaurus": "docusaurus", "start": "docusaurus start", - "build": "docusaurus build", + "build": "yarn pre-build && docusaurus build --locale en", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", @@ -16,7 +20,7 @@ "update-l10n-sources": "node scripts/update-l10n-sources.js", "lint": "prettier -c ./scripts/**/*.js", "test": "yarn lint && jest", - "prebuild": "node ./scripts/pre-build.js", + "pre-build": "node ./scripts/pre-build.js", "process-docs-changes": "node ./scripts/process-docs-changes.js", "update-pinned-version": "node ./scripts/update-pinned-version.js", "prepare": "husky install" @@ -50,6 +54,7 @@ "devDependencies": { "@actions/core": "^1.2.7", "@actions/github": "^4.0.0", + "@crowdin/cli": "3", "@types/jest": "^26.0.23", "@types/unist": "^2.0.3", "del": "^6.0.0", diff --git a/scripts/bin/azcopy b/scripts/bin/azcopy new file mode 100755 index 000000000..d85125960 Binary files /dev/null and b/scripts/bin/azcopy differ diff --git a/scripts/i18n-build.js b/scripts/i18n-build.js new file mode 100644 index 000000000..93d5f241b --- /dev/null +++ b/scripts/i18n-build.js @@ -0,0 +1,61 @@ +//@ts-check +const fs = require('fs').promises; +const { join } = require('path'); +const { execute } = require('./utils/execute'); +const { + i18n: { locales, defaultLocale }, +} = require('../docusaurus.config'); + +const updateConfig = async (locale) => { + const baseUrl = locale !== defaultLocale ? `/${locale}/` : '/'; + // Translations might not be completely in sync and we need to keep publishing + const onBrokenLinks = locale !== defaultLocale ? `warn` : `throw`; + const configPath = join(__dirname, '../docusaurus.config.js'); + + let docusaurusConfig = await fs.readFile(configPath, 'utf-8'); + + docusaurusConfig = docusaurusConfig + .replace(/baseUrl: '.*?',/, `baseUrl: '${baseUrl}',`) + .replace(/onBrokenLinks: '.*?',/, `onBrokenLinks: '${onBrokenLinks}',`); + + await fs.writeFile(configPath, docusaurusConfig, 'utf-8'); +}; + +const processLocale = async (locale) => { + const start = Date.now(); + const outdir = locale !== defaultLocale ? `--out-dir build/${locale}` : ''; + await execute(`yarn docusaurus build --locale ${locale} ${outdir}`); + console.log(`Locale ${locale} finished in ${(Date.now() - start) / 1000}s`); +}; + +/** + * + * @param {string} [locale] + */ +const start = async (locale) => { + const start = Date.now(); + + const localesToBuild = locale ? [locale] : locales; + + console.log('Building the following locales:'); + console.log(localesToBuild); + + for (const locale of localesToBuild) { + try { + await updateConfig(locale); + await processLocale(locale); + } catch (e) { + // We catch instead of just stopping the process because we want to restore docusaurus.config.js + console.error(e); + // TODO: It will be nice to do some clean up and point to the right file and line + console.error(`Locale ${locale} failed. Please check the logs above.`) + } + } + + // Restore `docusaurus.config.js` to the default values + await updateConfig(defaultLocale); + + console.log(`Process finished in ${(Date.now() - start) / 1000}s`); +}; + +start(process.argv[2]); diff --git a/scripts/prepare-i18n-content.js b/scripts/prepare-i18n-content.js new file mode 100644 index 000000000..dbf01606c --- /dev/null +++ b/scripts/prepare-i18n-content.js @@ -0,0 +1,47 @@ +//@ts-check + +/** + * Takes care of downloading the documentation from the + * right places, and transform it to make it ready to + * be used by docusaurus. + */ +const path = require('path'); +const fs = require('fs-extra'); + +const { addFrontmatter } = require('./tasks/add-frontmatter'); +const { fixContent } = require('./tasks/md-fixers'); + +const DOCS_FOLDER = path.join('docs', 'latest'); +const { + i18n: { locales: configuredLocales }, +} = require('../docusaurus.config'); + +const start = async () => { + const locales = new Set(configuredLocales); + locales.delete('en'); + for (const locale of locales) { + const localeDocs = path.join( + 'i18n', + locale, + 'docusaurus-plugin-content-docs', + 'current' + ); + const staticResources = ['fiddles', 'images']; + + console.log(`Copying static assets to ${locale}`); + for (const staticResource of staticResources) { + await fs.copy( + path.join(DOCS_FOLDER, staticResource), + path.join(localeDocs, 'latest', staticResource) + ); + } + + console.log(`Fixing markdown (${locale})`); + await fixContent(localeDocs, 'latest'); + + console.log(`Adding automatic frontmatter (${locale})`); + await addFrontmatter(path.join(localeDocs, 'latest')); + } +}; + +start(); diff --git a/scripts/process-docs-changes.js b/scripts/process-docs-changes.js index 1f6d92d80..7211273f5 100644 --- a/scripts/process-docs-changes.js +++ b/scripts/process-docs-changes.js @@ -11,6 +11,7 @@ if (!(process.env.CI || process.env.NODE_ENV === 'test') && !GITHUB_TOKEN) { process.exit(1); } +const { execute } = require('./utils/execute'); const { createPR, getChanges, pushChanges } = require('./utils/git-commands'); const HEAD = 'main'; @@ -42,9 +43,12 @@ const processDocsChanges = async () => { console.log('package.json is not modified, skipping'); return; } else { - const lines = output.split('\n'); - if (lines.length > 1) { - console.log(`New documents available, creating PR.`); + console.log(`Uploading changes to Crowdin`); + await execute(`yarn crowdin:upload`); + + // If there are new docs, `sidebars.js` gets updated so we check if there are changes + if (output.includes('M sidebars.js')) { + console.log(`New documents available.`); await createPR(PR_BRANCH, HEAD, EMAIL, NAME, COMMIT_MESSAGE); } else { console.log( diff --git a/scripts/tasks/add-frontmatter.js b/scripts/tasks/add-frontmatter.js index ed946257e..a7986cead 100644 --- a/scripts/tasks/add-frontmatter.js +++ b/scripts/tasks/add-frontmatter.js @@ -87,7 +87,7 @@ const descriptionFromContent = (content) => { // The content of structures is often only bullet lists and no general description if (trimmedLine.startsWith('#') || trimmedLine.startsWith('*')) { - if (subHeader) { + if (subHeader && description.length > 0) { return cleanUpMarkdown(description.trim()); } else { subHeader = true; @@ -122,7 +122,10 @@ const addFrontMatter = (content, filepath) => { ? titleMatches[1].trim() : titleFromPath(filepath).trim(); - const description = descriptionFromContent(content); + // The description of the files under `api/structures` is not meaningful so we ignore it + const description = filepath.includes('structures') + ? '' + : descriptionFromContent(content); const defaultSlug = path.basename(filepath, '.md'); let slug; diff --git a/scripts/tasks/md-fixers.js b/scripts/tasks/md-fixers.js index 532eb356b..756d3b55b 100644 --- a/scripts/tasks/md-fixers.js +++ b/scripts/tasks/md-fixers.js @@ -66,12 +66,31 @@ const fiddleTransformer = (line) => { if (matches) { return `\`\`\`fiddle docs/latest/${matches[1]}`; } else if (hasNewPath) { - return line.replace(fiddlePathFixRegex, '```fiddle docs/latest/'); + return ( + line + .replace(fiddlePathFixRegex, '```fiddle docs/latest/') + // we could have a double transformation if the path is already the good one + // this happens especially with the i18n content + .replace('latest/latest', 'latest') + ); } else { return line; } }; +/** + * Crowdin translations put markdown content right + * after HTML comments and thus breaking Docusaurus + * parse engine. We need to add a new EOL after `-->` + * is found. + */ +const newLineOnHTMLComment = (line) => { + if (line.includes('-->')) { + return line.replace('-->', '-->\n'); + } + return line; +}; + /** * Applies any transformation that can be executed line by line on * the document to make sure it is ready to be consumed by @@ -83,7 +102,11 @@ const fiddleTransformer = (line) => { const transform = (doc) => { const lines = doc.split('\n'); const newDoc = []; - const transformers = [apiTransformer, fiddleTransformer]; + const transformers = [ + apiTransformer, + fiddleTransformer, + newLineOnHTMLComment, + ]; for (const line of lines) { const newLine = transformers.reduce((newLine, transformer) => { diff --git a/scripts/update-l10n-sources.js b/scripts/update-l10n-sources.js index c6f3389b9..3065144f0 100644 --- a/scripts/update-l10n-sources.js +++ b/scripts/update-l10n-sources.js @@ -26,14 +26,13 @@ ${files.join('\n')}`); return; } - await del('i18n/en-US'); - await execute('yarn write-translations --locale en-US'); + await execute('yarn write-translations --locale en'); const localeModified = (await getChanges()) !== output; if (localeModified) { const pleaseCommit = - 'Contents in "/i18n/en-US/" have been modified. Please add the changes to your commit'; + 'Contents in "/i18n/en/" have been modified. Please add the changes to your commit'; console.error('\x1b[31m%s\x1b', pleaseCommit); process.exit(1); } diff --git a/yarn.lock b/yarn.lock index 7de7b6776..53a0a1837 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1310,6 +1310,13 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@crowdin/cli@3": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@crowdin/cli/-/cli-3.7.0.tgz#d35b69e90b6a737a9de017423ac34c02291cdebb" + integrity sha512-7eje7V6BGMeW23ywbrYdvpdIIxG5O1WP2wit4MVP9EtuZMOfr1M0l9BnObbkSYK86UiZuoJFHs1Q1KoCWg1rlA== + dependencies: + shelljs "^0.8.4" + "@docsearch/css@3.0.0-alpha.39": version "3.0.0-alpha.39" resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.0.0-alpha.39.tgz#1ebd390d93e06aad830492f5ffdc8e05d058813f"