Skip to content

Commit

Permalink
Add a local create-translation workflow
Browse files Browse the repository at this point in the history
  • Loading branch information
gaearon committed Apr 29, 2023
1 parent ee7bc2b commit 851a436
Show file tree
Hide file tree
Showing 15 changed files with 6,423 additions and 331 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/sync-batch-1.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ jobs:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
- run: yarn install
- run: ACTIONS_BATCH_PATTERN=6:1 yarn sync-forks
- run: ACTIONS_BATCH_PATTERN=6:1 yarn sync-translations
2 changes: 1 addition & 1 deletion .github/workflows/sync-batch-2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ jobs:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
- run: yarn install
- run: ACTIONS_BATCH_PATTERN=6:2 yarn sync-forks
- run: ACTIONS_BATCH_PATTERN=6:2 yarn sync-translations
2 changes: 1 addition & 1 deletion .github/workflows/sync-batch-3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ jobs:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
- run: yarn install
- run: ACTIONS_BATCH_PATTERN=6:3 yarn sync-forks
- run: ACTIONS_BATCH_PATTERN=6:3 yarn sync-translations
2 changes: 1 addition & 1 deletion .github/workflows/sync-batch-4.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ jobs:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
- run: yarn install
- run: ACTIONS_BATCH_PATTERN=6:4 yarn sync-forks
- run: ACTIONS_BATCH_PATTERN=6:4 yarn sync-translations
2 changes: 1 addition & 1 deletion .github/workflows/sync-batch-5.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ jobs:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
- run: yarn install
- run: ACTIONS_BATCH_PATTERN=6:5 yarn sync-forks
- run: ACTIONS_BATCH_PATTERN=6:5 yarn sync-translations
2 changes: 1 addition & 1 deletion .github/workflows/sync-batch-6.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ jobs:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
- run: yarn install
- run: ACTIONS_BATCH_PATTERN=6:6 yarn sync-forks
- run: ACTIONS_BATCH_PATTERN=6:6 yarn sync-translations
16 changes: 10 additions & 6 deletions PROGRESS.template.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
## Maintainer List

{MAINTAINERS}

## For New Translators

To translate a page:
Expand All @@ -19,13 +15,13 @@ Please be prompt with your translations! If you find that you can't commit anymo
When someone volunteers, edit this issue with the username of the volunteer, and with the PR. Ex:

```
- [ ] Quick Start (@tesseralis) #12345
- [ ] Some Page (@exampleusername) #12345
```

When PRs are merged, make sure to mark that page as completed like this:

```
- [x] Quick Start (@tesseralis) #12345
- [x] Some Page (@exampleusername) #12345
```

This ensures your translation's progress is tracked correctly at https://translations.react.dev/.
Expand Down Expand Up @@ -179,3 +175,11 @@ These aren't the main translation targets, but if you'd like to do them, feel fr
- [ ] Community
- [ ] Blog
- [ ] Warnings

## Maintainer List

This translation is maintained by:

{MAINTAINERS}

If you want to become a maintainer, ask them to add you. If the original maintainers are no longer responsive, raise an issue in the [main translations repository](https://github.com/reactjs/translations.react.dev).
25 changes: 12 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,30 @@ Check [translations.react.dev](https://translations.react.dev/) to see if your l

## Starting a new translation

If you would like to be the maintainer of a new translation, submit a PR adding a new file `{lang-code}.json`
to the `langs` folder with the following information:
If you would like to be the maintainer of a new translation, submit a PR adding it to the list in `langs/langs.json` with the following information:

* Language name (in English please)
* [Language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
* List of maintainers
* Language name (in your language)
* Language name (in English)

For example:

```json
{
"name": "English",
"code": "en",
"maintainers": ["gaearon", "tesseralis"]
}
{ "code": "fr", "name": "Français", "enName": "French" },
```

In the PR comment, please describe your experiences with translation (e.g. links to previous work). We prefer more than one maintainer on each repo, so if you're by yourself, we'll leave the PR open for others to join in. If you are a group, please have at least one person other than the PR opener comment, to make sure all people listed actually want to be part of the translation!
In the PR comment, please describe your experiences with translation (e.g. links to previous work) and mention all the initial translation maintainers. We prefer more than one maintainer on each repo, so if you're by yourself, we'll leave the PR open for others to join in. If you are a group, please have at least one person other than the PR opener comment, to make sure all people listed actually want to be part of the translation!

Also, please read the [Maintainer Responsibilities](/maintainer-guide.md#maintainer-responsibilities) and make sure that you are comfortable with the responsibilities listed.

Once the PR is accepted, the bot will:
Once the PR is accepted, a member of the React team will [run a script](/scripts/README.md#creating-a-translation) which will:

* Create a new repository for you at `reactjs/{lang-code}.react.dev`
* Add/invite all maintainers listed to a "react.dev {language} Translation" team in the reactjs organization
* Add/invite all maintainers you provided in the PR comment as administrators of the new repo
* Create an issue from [PROGRESS.template.md](/PROGRESS.template.md) in the new repository to track your translation progress

File an issue on this repository to apply for a real `{lang-code}.react.dev` subdomain once you have a few sections translated and can show sustained progress. Until then, the translation will be hosted at a preview domain.

If you are not a member of the reactjs organization, you should receive an email invite to join. Please accept this invite so you can get admin access to your repository!

You may want to [pin](https://help.github.com/articles/pinning-an-issue-to-your-repository/) the generated issue to make it easier to find.
Expand All @@ -45,10 +42,12 @@ Happy translating!

## Adding a maintainer

If you are currently a maintainer of a translation and want to add another member, send a pull request to this repo updating `langs/{lang-code}.json`, where `{lang-code}` is the language code of the repo you want to be a maintainer of.
If you are currently a maintainer of a translation and want to add another member, do it directly in the Settings panel of your repo.

If you are interested in becoming a maintainer for a translation, please ask one of the current maintainers if they would like to add you. While different maintainers can have different requirements, usually they look for people who have already contributed to the translation already, either by translating or reviewing.

If the translation's existing maintainers become unresponsible for more than a month, please raise an issue on this repository. If you don't receive a response in a week, please escalate the issue to the main React repository.

## Before publishing

1. Review your translations and make sure that the pages listed in "Main Content" are fully translated. Run the site yourself locally to make sure there are no bugs or missing translations.
Expand Down
3 changes: 2 additions & 1 deletion langs/langs.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@
{ "code": "uk", "name": "Українська", "enName": "Ukrainian" },
{ "code": "vi", "name": "Tiếng Việt", "enName": "Vietnamese" },
{ "code": "zh-hans", "name": "简体中文", "enName": "Simplified Chinese" },
{ "code": "zh-hant", "name": "繁體中文", "enName": "Traditional Chinese" }
{ "code": "zh-hant", "name": "繁體中文", "enName": "Traditional Chinese" },
{ "code": "test", "name": "Testtest", "enName": "Test Test TEst" }
]
25 changes: 23 additions & 2 deletions scripts/README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
# Translation Scripts

## Content Sync
## Syncing Translations

This script syncs changes from the main [reactjs/react.dev](https://github.com/reactjs/react.dev) repo to various translation forks. Configuration for the translation forks is located in the `langs` folder of this repository.

Sync scripts are run weekly via GitHub Actions. Pull requests are created by the shared [@react-translations-bot](https://github.com/react-translations-bot) account. This bot should be granted *write* permissions to all new language forks.
Sync scripts are run weekly via GitHub Actions. Pull requests are created by the shared [@react-translations-bot](https://github.com/react-translations-bot) account. This bot is normally granted *write* permissions to all new language forks.

To run the sync script locally, first copy the `.env.sample` to `.env` and fill in all environment variables.

Then to sync all languages, run:
```sh
yarn
yarn sync-forks
```

To sync only specific languages, run:
```sh
yarn
yarn sync-forks --langs=foo,bar
```

By default, the sync runs from `.github/workflow` and syncs languages in batches.

## Creating a Translation

A bot cannot create a translation, only people with org access can. Normally, React team members should monitor this repository, but feel free to raise an issue on the main React repo if a translation attempt is being unattended.

If you're a member of React team, here's what you need to do:

1. Install `gh` (GitHub CLI) and do `gh auth login`
2. Run `npx vercel login` and ensure you have permissions to create projects in Meta Open Source org
3. Ensure you have permissions to create projects in the `reactjs` GitHub org

Take a look at the pull request associated with the translation. It should be adding it to the `langs/langs.json` file. Suppose we're adding the `foo` language code with `@bar` and `@baz` GitHub users as maintainers. Merge the pull request and run in this folder:

```sh
yarn
yarn create-translations --lang=foo --maintainers=bar,baz
```

This should create the GitHub repo, create a Vercel project, link them, and deploy them. This won't by itself set up the domain--that can wait until the translation is mature.
164 changes: 164 additions & 0 deletions scripts/create-translation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
*
* Manual usage: yarn create-translation --lang=some-language-code --maintainers=gaearon,sophiebits
*
* NOTE: Unlike sync-translations.js, this script is meant to execute with your
* own local credentials. We don't want to give org admin access to the bot.
*
* You need to be logged into:
* - vercel (`npx vercel login`)
* - gh (`gh auth login`)
*/
const fs = require('fs');
const yaml = require('js-yaml');
const os = require('os');
const path = require('path');
const { spawn } = require('child_process');
const { Octokit } = require('@octokit/rest');
const { remove } = require("fs-extra");
const commander = require("commander");
const allLanguages = require("../langs/langs.json");

commander
.option('--lang <languageCode>', 'Language code')
.option('--maintainers <maintainers>', 'Maintainers list (comma-separated)')
.parse(process.argv);

if (!commander.lang || !commander.maintainers) {
throw Error('Both --lang and --maintainers arguments are required.');
}

const GITHUB_ORG = "reactjs";
const MAIN_REPOSITORY_NAME = "react.dev";
const botUsername = 'react-translations-bot'; // Given permissions on new repos.

function getGithubConfig() {
const configPath = path.join(os.homedir(), '.config/gh/hosts.yml');
const configFile = fs.readFileSync(configPath, 'utf8');
const config = yaml.load(configFile);
return config['github.com'];
}

let ghConfig;
try {
ghConfig = getGithubConfig();
} catch (e) {
// NOTE: this script always uses the `gh` CLI.
// We intentionally do NOT try to log in with the bot's token
// because the bot should not have the rights to admin the entire org.
// So this script needs to be run by someone who has admin rights.
throw Error(
'Could not read GH token from `gh`.\n' +
'Make sure you installed `gh` and ran `gh auth login` first.'
);
}
const octokit = new Octokit({ auth: ghConfig.oauth_token });

const languageCode = commander.lang.trim();
const maintainers = commander.maintainers.split(',').map(login => {
if (login.startsWith('@')) {
return login.slice(1);
} else {
return login;
}
});
if (maintainers.length === 0) {
throw Error('At least one maintainer must be specified.');
}

const languageInfo = allLanguages.find(l => l.code === languageCode);
if (!languageInfo) {
throw Error(
'Could not find language "' + languageCode + '" in langs/langs.json. ' +
'Merge the pull request before running this script.'
);
}

async function execWithOutput(cmd, args, options) {
return new Promise((resolve, reject) => {
const child = spawn(cmd, args, { stdio: 'inherit', ...options });
child.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(code);
}
});
});
}

async function main() {
// Let's verify you're logged in.
await execWithOutput('gh', ['auth', 'status']);
await execWithOutput('./node_modules/.bin/vercel', ['whoami']);

// We're going to clone the original repo first, but push it to the new remote.
console.log('\nCloning the original repo...');
const newRepoName = `${languageCode}.${MAIN_REPOSITORY_NAME}`;
const folderName = '.temp/' + newRepoName.replaceAll('.', '-');
await remove(folderName);
await execWithOutput('git', ['clone', `https://github.com/${GITHUB_ORG}/${MAIN_REPOSITORY_NAME}`, folderName]);

// Create the new repo, make it an origin, and push to it.
console.log('\nCreating GitHub repo:' + newRepoName + '...');
await octokit.repos.createInOrg({
org: GITHUB_ORG,
name: newRepoName,
description: `(Work in progress) React documentation website in ${languageInfo.enName}`,
allow_squash_merge: false,
allow_merge_commit: true,
allow_rebase_merge: false,
delete_branch_on_merge: true,
});
await octokit.repos.addCollaborator({
owner: GITHUB_ORG,
repo: newRepoName,
username: botUsername,
permission: 'push', // To create sync branches
});
for (const maintainer of maintainers) {
await octokit.repos.addCollaborator({
owner: GITHUB_ORG,
repo: newRepoName,
username: maintainer,
permission: 'admin',
});
}
console.log('\nPushing to the new repository...');
const newRemote = `https://github.com/${GITHUB_ORG}/${newRepoName}.git`;
const newRemoteWithLoginToken = `${ghConfig.git_protocol}://${ghConfig.user}:${ghConfig.oauth_token}@github.com/${GITHUB_ORG}/${newRepoName}.git`;
await execWithOutput('git', ['push', newRemoteWithLoginToken], { cwd: folderName });
await execWithOutput('git', ['remote', 'set-url', 'origin', newRemote], { cwd: folderName });
// Now let's set up a Vercel project, link it, and trigger the build.
console.log('\nCreating a Vercel project...');
await execWithOutput('npx', ['vercel', 'link', '--scope=fbopensource', '--yes'], { cwd: folderName });
console.log('\nConnecting the Vercel project to GitHub...');
await execWithOutput('npx', ['vercel', 'git', 'connect', '--yes'], { cwd: folderName });

// Edit README.md and push to kick off the build
const readmePath = `${folderName}/README.md`;
const originalReadmeContent = fs.readFileSync(readmePath, 'utf8');
const updatedReadmeContent = originalReadmeContent.replaceAll('react.dev', `${languageCode}.react.dev`);
fs.writeFileSync(readmePath, updatedReadmeContent, 'utf8');
await execWithOutput('git', ['commit', '-am', 'Set up the translation'], { cwd: folderName });
await execWithOutput('git', ['push', newRemoteWithLoginToken], { cwd: folderName });
await remove(folderName);

console.log('\nCreating an issue to track translation progress...');
// Create the progress-tracking issue from the template
const issueTemplate = fs.readFileSync(__dirname + '/../PROGRESS.template.md', 'utf8');
const issueBody = issueTemplate.replaceAll(
'{MAINTAINERS}',
maintainers.map(login => ' - @' + login).join('\n')
);
await octokit.issues.create({
owner: GITHUB_ORG,
repo: newRepoName,
title: `${languageInfo.enName} Translation Progress`,
body: issueBody,
});
console.log('\nCreated issue to track translation progress.');
console.log('\nWe are done here.')
}

main();
Empty file removed scripts/create.js
Empty file.
7 changes: 5 additions & 2 deletions scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"license": "MIT",
"scripts": {
"prettier": "prettier --write *.js",
"sync-forks": "node sync-forks.js"
"create-translation": "node create-translation.js",
"sync-translations": "node sync-translations.js"
},
"dependencies": {
"@octokit/rest": "^16.15.0",
Expand All @@ -15,9 +16,11 @@
"cron": "^1.6.0",
"dotenv": "^10.0.0",
"fs-extra": "^10.0.0",
"js-yaml": "^4.1.0",
"log4js": "^4.0.2",
"progress-estimator": "^0.3.0",
"shelljs": "^0.7.8"
"shelljs": "^0.7.8",
"vercel": "^29.0.0"
},
"engines": {
"node.js": "11.9.0"
Expand Down
4 changes: 3 additions & 1 deletion scripts/sync-forks.js → scripts/sync-translations.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
*
* Manual usage: yarn sync-forks --langs=es,zh-hans
* Manual usage: yarn sync-translations --langs=es,zh-hans
* This script is usually called by .github/workflows/*.
*
*/
Expand Down Expand Up @@ -228,6 +228,8 @@ async function syncContentWithUpstream(languageCode) {
const mainRepoUrl = `https://github.com/${GITHUB_ORG}/${MAIN_REPOSITORY_NAME}`;
const options = { cwd: repoPath };

// Avoid storing these in Keychain.
await exec('git config credential.helper ""', options);
await exec(`git config user.name ${GITHUB_USER_NAME}`, options);
await exec(`git config user.email ${GITHUB_USER_EMAIL}`, options);
await exec(`git remote set-url origin ${repoURL}`, options);
Expand Down
Loading

0 comments on commit 851a436

Please sign in to comment.