Skip to content

Commit 06af930

Browse files
committed
Add a local create-translation workflow
1 parent ee7bc2b commit 06af930

15 files changed

+6425
-331
lines changed

.github/workflows/sync-batch-1.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ jobs:
3131
node-version: ${{ matrix.node-version }}
3232
cache: 'yarn'
3333
- run: yarn install
34-
- run: ACTIONS_BATCH_PATTERN=6:1 yarn sync-forks
34+
- run: ACTIONS_BATCH_PATTERN=6:1 yarn sync-translations

.github/workflows/sync-batch-2.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ jobs:
3131
node-version: ${{ matrix.node-version }}
3232
cache: 'yarn'
3333
- run: yarn install
34-
- run: ACTIONS_BATCH_PATTERN=6:2 yarn sync-forks
34+
- run: ACTIONS_BATCH_PATTERN=6:2 yarn sync-translations

.github/workflows/sync-batch-3.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ jobs:
3131
node-version: ${{ matrix.node-version }}
3232
cache: 'yarn'
3333
- run: yarn install
34-
- run: ACTIONS_BATCH_PATTERN=6:3 yarn sync-forks
34+
- run: ACTIONS_BATCH_PATTERN=6:3 yarn sync-translations

.github/workflows/sync-batch-4.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ jobs:
3131
node-version: ${{ matrix.node-version }}
3232
cache: 'yarn'
3333
- run: yarn install
34-
- run: ACTIONS_BATCH_PATTERN=6:4 yarn sync-forks
34+
- run: ACTIONS_BATCH_PATTERN=6:4 yarn sync-translations

.github/workflows/sync-batch-5.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ jobs:
3131
node-version: ${{ matrix.node-version }}
3232
cache: 'yarn'
3333
- run: yarn install
34-
- run: ACTIONS_BATCH_PATTERN=6:5 yarn sync-forks
34+
- run: ACTIONS_BATCH_PATTERN=6:5 yarn sync-translations

.github/workflows/sync-batch-6.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ jobs:
3131
node-version: ${{ matrix.node-version }}
3232
cache: 'yarn'
3333
- run: yarn install
34-
- run: ACTIONS_BATCH_PATTERN=6:6 yarn sync-forks
34+
- run: ACTIONS_BATCH_PATTERN=6:6 yarn sync-translations

PROGRESS.template.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
## Maintainer List
2-
3-
{MAINTAINERS}
4-
51
## For New Translators
62

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

2117
```
22-
- [ ] Quick Start (@tesseralis) #12345
18+
- [ ] Some Page (@exampleusername) #12345
2319
```
2420

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

2723
```
28-
- [x] Quick Start (@tesseralis) #12345
24+
- [x] Some Page (@exampleusername) #12345
2925
```
3026

3127
This ensures your translation's progress is tracked correctly at https://translations.react.dev/.
@@ -179,3 +175,11 @@ These aren't the main translation targets, but if you'd like to do them, feel fr
179175
- [ ] Community
180176
- [ ] Blog
181177
- [ ] Warnings
178+
179+
## Maintainer List
180+
181+
This translation is maintained by:
182+
183+
{MAINTAINERS}
184+
185+
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).

README.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,33 +8,32 @@ Check [translations.react.dev](https://translations.react.dev/) to see if your l
88

99
## Starting a new translation
1010

11-
If you would like to be the maintainer of a new translation, submit a PR adding a new file `{lang-code}.json`
12-
to the `langs` folder with the following information:
11+
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:
1312

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

1817
For example:
1918

2019
```json
21-
{
22-
"name": "English",
23-
"code": "en",
24-
"maintainers": ["gaearon", "tesseralis"]
25-
}
20+
// ...
21+
{ "code": "fr", "name": "Français", "enName": "French" },
22+
// ...
2623
```
2724

28-
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!
25+
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!
2926

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

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

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

35+
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.
36+
3837
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!
3938

4039
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.
@@ -45,10 +44,12 @@ Happy translating!
4544

4645
## Adding a maintainer
4746

48-
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.
47+
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.
4948

5049
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.
5150

51+
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.
52+
5253
## Before publishing
5354

5455
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.

langs/langs.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@
2020
{ "code": "uk", "name": "Українська", "enName": "Ukrainian" },
2121
{ "code": "vi", "name": "Tiếng Việt", "enName": "Vietnamese" },
2222
{ "code": "zh-hans", "name": "简体中文", "enName": "Simplified Chinese" },
23-
{ "code": "zh-hant", "name": "繁體中文", "enName": "Traditional Chinese" }
23+
{ "code": "zh-hant", "name": "繁體中文", "enName": "Traditional Chinese" },
24+
{ "code": "test", "name": "Testtest", "enName": "Test Test TEst" }
2425
]

scripts/README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,42 @@
11
# Translation Scripts
22

3-
## Content Sync
3+
## Syncing Translations
44

55
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.
66

7-
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.
7+
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.
88

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

1111
Then to sync all languages, run:
1212
```sh
13+
yarn
1314
yarn sync-forks
1415
```
1516

1617
To sync only specific languages, run:
1718
```sh
19+
yarn
1820
yarn sync-forks --langs=foo,bar
1921
```
2022

2123
By default, the sync runs from `.github/workflow` and syncs languages in batches.
24+
25+
## Creating a Translation
26+
27+
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.
28+
29+
If you're a member of React team, here's what you need to do:
30+
31+
1. Install `gh` (GitHub CLI) and do `gh auth login`
32+
2. Run `npx vercel login` and ensure you have permissions to create projects in Meta Open Source org
33+
3. Ensure you have permissions to create projects in the `reactjs` GitHub org
34+
35+
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:
36+
37+
```sh
38+
yarn
39+
yarn create-translations --lang=foo --maintainers=bar,baz
40+
```
41+
42+
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.

scripts/create-translation.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
*
3+
* Manual usage: yarn create-translation --lang=some-language-code --maintainers=gaearon,sophiebits
4+
*
5+
* NOTE: Unlike sync-translations.js, this script is meant to execute with your
6+
* own local credentials. We don't want to give org admin access to the bot.
7+
*
8+
* You need to be logged into:
9+
* - vercel (`npx vercel login`)
10+
* - gh (`gh auth login`)
11+
*/
12+
const fs = require('fs');
13+
const yaml = require('js-yaml');
14+
const os = require('os');
15+
const path = require('path');
16+
const { spawn } = require('child_process');
17+
const { Octokit } = require('@octokit/rest');
18+
const { remove } = require("fs-extra");
19+
const commander = require("commander");
20+
const allLanguages = require("../langs/langs.json");
21+
22+
commander
23+
.option('--lang <languageCode>', 'Language code')
24+
.option('--maintainers <maintainers>', 'Maintainers list (comma-separated)')
25+
.parse(process.argv);
26+
27+
if (!commander.lang || !commander.maintainers) {
28+
throw Error('Both --lang and --maintainers arguments are required.');
29+
}
30+
31+
const GITHUB_ORG = "reactjs";
32+
const MAIN_REPOSITORY_NAME = "react.dev";
33+
const botUsername = 'react-translations-bot'; // Given permissions on new repos.
34+
35+
function getGithubConfig() {
36+
const configPath = path.join(os.homedir(), '.config/gh/hosts.yml');
37+
const configFile = fs.readFileSync(configPath, 'utf8');
38+
const config = yaml.load(configFile);
39+
return config['github.com'];
40+
}
41+
42+
let ghConfig;
43+
try {
44+
ghConfig = getGithubConfig();
45+
} catch (e) {
46+
// NOTE: this script always uses the `gh` CLI.
47+
// We intentionally do NOT try to log in with the bot's token
48+
// because the bot should not have the rights to admin the entire org.
49+
// So this script needs to be run by someone who has admin rights.
50+
throw Error(
51+
'Could not read GH token from `gh`.\n' +
52+
'Make sure you installed `gh` and ran `gh auth login` first.'
53+
);
54+
}
55+
const octokit = new Octokit({ auth: ghConfig.oauth_token });
56+
57+
const languageCode = commander.lang.trim();
58+
const maintainers = commander.maintainers.split(',').map(login => {
59+
if (login.startsWith('@')) {
60+
return login.slice(1);
61+
} else {
62+
return login;
63+
}
64+
});
65+
if (maintainers.length === 0) {
66+
throw Error('At least one maintainer must be specified.');
67+
}
68+
69+
const languageInfo = allLanguages.find(l => l.code === languageCode);
70+
if (!languageInfo) {
71+
throw Error(
72+
'Could not find language "' + languageCode + '" in langs/langs.json. ' +
73+
'Merge the pull request before running this script.'
74+
);
75+
}
76+
77+
async function execWithOutput(cmd, args, options) {
78+
return new Promise((resolve, reject) => {
79+
const child = spawn(cmd, args, { stdio: 'inherit', ...options });
80+
child.on('close', (code) => {
81+
if (code === 0) {
82+
resolve();
83+
} else {
84+
reject(code);
85+
}
86+
});
87+
});
88+
}
89+
90+
async function main() {
91+
// Let's verify you're logged in.
92+
await execWithOutput('gh', ['auth', 'status']);
93+
await execWithOutput('./node_modules/.bin/vercel', ['whoami']);
94+
95+
// We're going to clone the original repo first, but push it to the new remote.
96+
console.log('\nCloning the original repo...');
97+
const newRepoName = `${languageCode}.${MAIN_REPOSITORY_NAME}`;
98+
const folderName = '.temp/' + newRepoName.replaceAll('.', '-');
99+
await remove(folderName);
100+
await execWithOutput('git', ['clone', `https://github.com/${GITHUB_ORG}/${MAIN_REPOSITORY_NAME}`, folderName]);
101+
102+
// Create the new repo, make it an origin, and push to it.
103+
console.log('\nCreating GitHub repo:' + newRepoName + '...');
104+
await octokit.repos.createInOrg({
105+
org: GITHUB_ORG,
106+
name: newRepoName,
107+
description: `(Work in progress) React documentation website in ${languageInfo.enName}`,
108+
allow_squash_merge: false,
109+
allow_merge_commit: true,
110+
allow_rebase_merge: false,
111+
delete_branch_on_merge: true,
112+
});
113+
await octokit.repos.addCollaborator({
114+
owner: GITHUB_ORG,
115+
repo: newRepoName,
116+
username: botUsername,
117+
permission: 'push', // To create sync branches
118+
});
119+
for (const maintainer of maintainers) {
120+
await octokit.repos.addCollaborator({
121+
owner: GITHUB_ORG,
122+
repo: newRepoName,
123+
username: maintainer,
124+
permission: 'admin',
125+
});
126+
}
127+
console.log('\nPushing to the new repository...');
128+
const newRemote = `https://github.com/${GITHUB_ORG}/${newRepoName}.git`;
129+
const newRemoteWithLoginToken = `${ghConfig.git_protocol}://${ghConfig.user}:${ghConfig.oauth_token}@github.com/${GITHUB_ORG}/${newRepoName}.git`;
130+
await execWithOutput('git', ['push', newRemoteWithLoginToken], { cwd: folderName });
131+
await execWithOutput('git', ['remote', 'set-url', 'origin', newRemote], { cwd: folderName });
132+
// Now let's set up a Vercel project, link it, and trigger the build.
133+
console.log('\nCreating a Vercel project...');
134+
await execWithOutput('npx', ['vercel', 'link', '--scope=fbopensource', '--yes'], { cwd: folderName });
135+
console.log('\nConnecting the Vercel project to GitHub...');
136+
await execWithOutput('npx', ['vercel', 'git', 'connect', '--yes'], { cwd: folderName });
137+
138+
// Edit README.md and push to kick off the build
139+
const readmePath = `${folderName}/README.md`;
140+
const originalReadmeContent = fs.readFileSync(readmePath, 'utf8');
141+
const updatedReadmeContent = originalReadmeContent.replaceAll('react.dev', `${languageCode}.react.dev`);
142+
fs.writeFileSync(readmePath, updatedReadmeContent, 'utf8');
143+
await execWithOutput('git', ['commit', '-am', 'Set up the translation'], { cwd: folderName });
144+
await execWithOutput('git', ['push', newRemoteWithLoginToken], { cwd: folderName });
145+
await remove(folderName);
146+
147+
console.log('\nCreating an issue to track translation progress...');
148+
// Create the progress-tracking issue from the template
149+
const issueTemplate = fs.readFileSync(__dirname + '/../PROGRESS.template.md', 'utf8');
150+
const issueBody = issueTemplate.replaceAll(
151+
'{MAINTAINERS}',
152+
maintainers.map(login => ' - @' + login).join('\n')
153+
);
154+
await octokit.issues.create({
155+
owner: GITHUB_ORG,
156+
repo: newRepoName,
157+
title: `${languageInfo.enName} Translation Progress`,
158+
body: issueBody,
159+
});
160+
console.log('\nCreated issue to track translation progress.');
161+
console.log('\nWe are done here.')
162+
}
163+
164+
main();

scripts/create.js

Whitespace-only changes.

scripts/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"license": "MIT",
66
"scripts": {
77
"prettier": "prettier --write *.js",
8-
"sync-forks": "node sync-forks.js"
8+
"create-translation": "node create-translation.js",
9+
"sync-translations": "node sync-translations.js"
910
},
1011
"dependencies": {
1112
"@octokit/rest": "^16.15.0",
@@ -15,9 +16,11 @@
1516
"cron": "^1.6.0",
1617
"dotenv": "^10.0.0",
1718
"fs-extra": "^10.0.0",
19+
"js-yaml": "^4.1.0",
1820
"log4js": "^4.0.2",
1921
"progress-estimator": "^0.3.0",
20-
"shelljs": "^0.7.8"
22+
"shelljs": "^0.7.8",
23+
"vercel": "^29.0.0"
2124
},
2225
"engines": {
2326
"node.js": "11.9.0"

scripts/sync-forks.js renamed to scripts/sync-translations.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
*
3-
* Manual usage: yarn sync-forks --langs=es,zh-hans
3+
* Manual usage: yarn sync-translations --langs=es,zh-hans
44
* This script is usually called by .github/workflows/*.
55
*
66
*/
@@ -228,6 +228,8 @@ async function syncContentWithUpstream(languageCode) {
228228
const mainRepoUrl = `https://github.com/${GITHUB_ORG}/${MAIN_REPOSITORY_NAME}`;
229229
const options = { cwd: repoPath };
230230

231+
// Avoid storing these in Keychain.
232+
await exec('git config credential.helper ""', options);
231233
await exec(`git config user.name ${GITHUB_USER_NAME}`, options);
232234
await exec(`git config user.email ${GITHUB_USER_EMAIL}`, options);
233235
await exec(`git remote set-url origin ${repoURL}`, options);

0 commit comments

Comments
 (0)