Skip to content

Commit

Permalink
feat: Crowdin integration
Browse files Browse the repository at this point in the history
Integrates with Crowdin to enable i18n of the site. This PR changes the
source of truth for Crowdin and moves it to this repository instead of
relying on `electron/i18n`.

Additionally it uses Crowdin's CLI to do the upload/download of assets
making it more reliable.

The website is built on locale at a time via `yarn i18n:build`.
Otherwise the process crashed with an out of memory error. The regular
`yarn build` command still compiles the `en` locale.

Because we cannot get notifications when there are new translations
avaiable, there is a GitHub workflow (`update-i18n-deploy.yml`) that
downloads the content every few minutes, builds, and deploy. To speed up
this process, the previous generated assets are download. In local tests
this reduces the build times from 250s to 40s so the whole process
should take about 5 minutes.

The previous generated content is stored in Azure Storage. Because this
is a static website it makes more sense than having a dyno and will make
it easier to:
- deploy multiple locales at the same time if we still need to speed up
  the process
- have versioned docs because we just need to "take a snapshot" and
  publish to a different folder

The current live site is still not using this storage but will soon-ish.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Fix #64
  • Loading branch information
molant committed Oct 7, 2021
1 parent b6fc413 commit b4e995b
Show file tree
Hide file tree
Showing 18 changed files with 248 additions and 16 deletions.
38 changes: 38 additions & 0 deletions .github/workflows/update-i18n-deploy.yml
Original file line number Diff line number Diff line change
@@ -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 }}
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ node_modules
.env
.vscode/settings.json
build/
content/
content/
i18n/
!i18n/en/
21 changes: 21 additions & 0 deletions crowdin.yml
Original file line number Diff line number Diff line change
@@ -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%',
},
]
18 changes: 15 additions & 3 deletions docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -52,6 +56,10 @@ module.exports = {
label: 'Releases',
position: 'right',
},
{
type: 'localeDropdown',
position: 'right',
},
{
href: 'https://github.com/electron/electron',
label: 'GitHub',
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions i18n/en-US/code.json → i18n/en/code.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand Down Expand Up @@ -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",
Expand Down
Binary file added scripts/bin/azcopy
Binary file not shown.
61 changes: 61 additions & 0 deletions scripts/i18n-build.js
Original file line number Diff line number Diff line change
@@ -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]);
47 changes: 47 additions & 0 deletions scripts/prepare-i18n-content.js
Original file line number Diff line number Diff line change
@@ -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();
10 changes: 7 additions & 3 deletions scripts/process-docs-changes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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(
Expand Down
7 changes: 5 additions & 2 deletions scripts/tasks/add-frontmatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
29 changes: 27 additions & 2 deletions scripts/tasks/md-fixers.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,33 @@ 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.
* @param {string} line
*/
const newLineOnHTMLComment = (line) => {
// The `startsWith('*')` part is to prevent messing the document `api/native-theme.md` 😓
if (line.includes('-->') && !line.endsWith('-->') && !line.startsWith('*')) {
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
Expand All @@ -83,7 +104,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) => {
Expand Down
5 changes: 2 additions & 3 deletions scripts/update-l10n-sources.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
7 changes: 7 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit b4e995b

Please sign in to comment.