feat: Crowdin integration scripts/actions (#40657)
This commit is contained in:
54
.github/workflows/crowdin-i18n-download.yml
vendored
Normal file
54
.github/workflows/crowdin-i18n-download.yml
vendored
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
name: Crowdin i18n Download Translations
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- test-crowdin-download
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
i18n-download-curriculum-translations:
|
||||||
|
name: Learn
|
||||||
|
runs-on: ubuntu-18.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Source Files
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
##### Chinese Download #####
|
||||||
|
- name: Crowdin Download for Chinese Translations
|
||||||
|
uses: crowdin/github-action@master
|
||||||
|
# options: https://github.com/crowdin/github-action/blob/master/action.yml
|
||||||
|
with:
|
||||||
|
# uploads
|
||||||
|
upload_sources: false
|
||||||
|
upload_translations: false
|
||||||
|
auto_approve_imported: false
|
||||||
|
import_eq_suggestions: false
|
||||||
|
|
||||||
|
# downloads
|
||||||
|
download_language: zh-CN
|
||||||
|
download_translations: true
|
||||||
|
skip_untranslated_files: true
|
||||||
|
export_only_approved: true
|
||||||
|
|
||||||
|
commit_message: 'chore(i8n,learn): processed chinese translations'
|
||||||
|
|
||||||
|
# pull-request
|
||||||
|
localization_branch_name: i18n-sync-learn-processed-chinese-translations
|
||||||
|
create_pull_request: false
|
||||||
|
pull_request_title: 'chore(i18n,learn): Processed chinese translations from crowdin'
|
||||||
|
pull_request_body: ''
|
||||||
|
pull_request_labels: 'scope: i18n, scope: learn, crowdin-sync, language: Chinese'
|
||||||
|
|
||||||
|
# global options
|
||||||
|
config: './curriculum/crowdin.yml'
|
||||||
|
base_url: ${{ secrets.CROWDIN_BASE_URL_FCC }}
|
||||||
|
|
||||||
|
# Uncomment below to debug
|
||||||
|
# dryrun_action: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID_CURRICULUM }}
|
||||||
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_CAMPERBOT_SERVICE_TOKEN }}
|
64
.github/workflows/crowdin-i18n-upload.yml
vendored
Normal file
64
.github/workflows/crowdin-i18n-upload.yml
vendored
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
name: Crowdin i18n Upload Action
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- test-crowdin-upload
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
i18n-upload-curriculum-files:
|
||||||
|
name: Learn
|
||||||
|
runs-on: ubuntu-18.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Source Files
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
working-directory: ./tools
|
||||||
|
run: |
|
||||||
|
cd ./crowdin
|
||||||
|
npm ci
|
||||||
|
|
||||||
|
- name: Crowdin Upload
|
||||||
|
uses: crowdin/github-action@master
|
||||||
|
# options: https://github.com/crowdin/github-action/blob/master/action.yml
|
||||||
|
with:
|
||||||
|
# uploads
|
||||||
|
upload_sources: true
|
||||||
|
upload_translations: true
|
||||||
|
auto_approve_imported: false
|
||||||
|
import_eq_suggestions: false
|
||||||
|
|
||||||
|
# downloads
|
||||||
|
download_translations: false
|
||||||
|
|
||||||
|
# pull-request
|
||||||
|
create_pull_request: false
|
||||||
|
|
||||||
|
# global options
|
||||||
|
config: './curriculum/crowdin.yml'
|
||||||
|
base_url: ${{ secrets.CROWDIN_BASE_URL_FCC }}
|
||||||
|
|
||||||
|
# Uncomment below to debug
|
||||||
|
# dryrun_action: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID_CURRICULUM }}
|
||||||
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_CAMPERBOT_SERVICE_TOKEN }}
|
||||||
|
|
||||||
|
- name: Remove Deleted English Curriculum Files From Crowdin
|
||||||
|
uses: ./tools/crowdin/actions/remove-deleted-files
|
||||||
|
env:
|
||||||
|
CROWDIN_API_URL: 'https://freecodecamp.crowdin.com/api/v2/'
|
||||||
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_CAMPERBOT_SERVICE_TOKEN }}
|
||||||
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID_CURRICULUM }}
|
||||||
|
|
||||||
|
- name: Hide Non-Translated Strings
|
||||||
|
uses: ./tools/crowdin/actions/hide-non-translated-strings
|
||||||
|
env:
|
||||||
|
CROWDIN_API_URL: 'https://freecodecamp.crowdin.com/api/v2/'
|
||||||
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_CAMPERBOT_SERVICE_TOKEN }}
|
||||||
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID_CURRICULUM }}
|
47
.github/workflows/crowdin-i18n.yml
vendored
47
.github/workflows/crowdin-i18n.yml
vendored
@ -1,47 +0,0 @@
|
|||||||
name: Crowdin Sync
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
# schedule:
|
|
||||||
# (TODO?) run this Action every 14 days
|
|
||||||
# - cron: '0 * */14 * *'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
i18n-sync-docs:
|
|
||||||
name: Docs
|
|
||||||
runs-on: ubuntu-18.04
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Source Files
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Crowdin Synchronize
|
|
||||||
uses: crowdin/github-action@master
|
|
||||||
# options: https://github.com/crowdin/github-action/blob/master/action.yml
|
|
||||||
with:
|
|
||||||
# uploads
|
|
||||||
upload_translations: false
|
|
||||||
auto_approve_imported: false
|
|
||||||
import_eq_suggestions: false
|
|
||||||
|
|
||||||
# downloads
|
|
||||||
download_translations: true
|
|
||||||
commit_message: 'chore(i8n,docs): processed translations from crowdin'
|
|
||||||
|
|
||||||
# pull-request
|
|
||||||
localization_branch_name: i18n-sync-docs
|
|
||||||
create_pull_request: true
|
|
||||||
pull_request_title: 'chore(i18n,docs): processed translations from crowdin'
|
|
||||||
pull_request_body: ''
|
|
||||||
pull_request_labels: 'scope: i18n, scope: docs, crowdin-sync'
|
|
||||||
|
|
||||||
# global options
|
|
||||||
config: './docs/crowdin.yml'
|
|
||||||
base_url: ${{ secrets.CROWDIN_BASE_URL_FCC }}
|
|
||||||
|
|
||||||
# Uncomment below to debug
|
|
||||||
# dryrun_action: true
|
|
||||||
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID_DOCS }}
|
|
||||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_CAMPERBOT_SERVICE_TOKEN }}
|
|
@ -1,4 +1,4 @@
|
|||||||
"project_id_env": "CROWDIN_PROJECT_ID_CURRICULUM"
|
"project_id_env": "CROWDIN_PROJECT_ID"
|
||||||
"api_token_env": "CROWDIN_PERSONAL_TOKEN"
|
"api_token_env": "CROWDIN_PERSONAL_TOKEN"
|
||||||
"base_path_env": "CROWDIN_BASE_PATH"
|
"base_path_env": "CROWDIN_BASE_PATH"
|
||||||
"base_url_env": "CROWDIN_BASE_URL"
|
"base_url_env": "CROWDIN_BASE_URL"
|
||||||
@ -7,27 +7,12 @@
|
|||||||
|
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
"source" : "/curriculum/challenges/english/**/*.md",
|
"source" : "/curriculum/challenges/english/01-responsive-web-design/**/*.md",
|
||||||
"translation" : "/curriculum/challenges/%language%/**/%original_file_name%",
|
"translation" : "/curriculum/challenges/%language%/01-responsive-web-design/**/%original_file_name%",
|
||||||
|
"ignore": [
|
||||||
# "languages_mapping" : {
|
"/**/part-[0-9][0-9][0-9].md",
|
||||||
# "language" : {
|
"/curriculum/challenges/english/[0-9][0-9]-certificates/**/*.*"
|
||||||
|
],
|
||||||
# }
|
|
||||||
# },
|
|
||||||
|
|
||||||
"update_option": "update_as_unapproved"
|
"update_option": "update_as_unapproved"
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": "/curriculum/dictionaries/english/**/*.js",
|
|
||||||
"translation": "/curriculum/dictionaries/%language%/**/%original_file_name%",
|
|
||||||
|
|
||||||
# "languages_mapping" : {
|
|
||||||
# "language" : {
|
|
||||||
|
|
||||||
# }
|
|
||||||
# },
|
|
||||||
|
|
||||||
"update_option": "update_without_changes"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
name: 'Hide Non-Translated Strings'
|
||||||
|
description: "Updates each file's non-translatable string to Hidden"
|
||||||
|
runs:
|
||||||
|
using: 'node12'
|
||||||
|
main: './index.js'
|
35
tools/crowdin/actions/hide-non-translated-strings/index.js
Normal file
35
tools/crowdin/actions/hide-non-translated-strings/index.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
require('dotenv').config({ path: `${__dirname}/../../.env` });
|
||||||
|
// const core = require('@actions/core');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const matter = require('gray-matter');
|
||||||
|
const { getFiles } = require('../../utils/files');
|
||||||
|
const { updateFileStrings } = require('../../utils/strings');
|
||||||
|
|
||||||
|
const hideNonTranslatedStrings = async projectId => {
|
||||||
|
console.log('start hiding non-translated strings...');
|
||||||
|
const crowdinFiles = await getFiles(projectId);
|
||||||
|
if (crowdinFiles && crowdinFiles.length) {
|
||||||
|
for (let { fileId, path: crowdinFilePath } of crowdinFiles) {
|
||||||
|
const challengeFilePath = path.join(
|
||||||
|
__dirname,
|
||||||
|
'/../../../../',
|
||||||
|
crowdinFilePath
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const challengeContent = fs.readFileSync(challengeFilePath);
|
||||||
|
const {
|
||||||
|
data: { title: challengeTitle }
|
||||||
|
} = matter(challengeContent);
|
||||||
|
await updateFileStrings({ projectId, fileId, challengeTitle });
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err.name);
|
||||||
|
console.log(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('hiding non-translated strings complete');
|
||||||
|
};
|
||||||
|
|
||||||
|
const projectId = process.env.CROWDIN_PROJECT_ID;
|
||||||
|
hideNonTranslatedStrings(projectId);
|
5
tools/crowdin/actions/remove-deleted-files/action.yml
Normal file
5
tools/crowdin/actions/remove-deleted-files/action.yml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
name: 'Remove Deleted English Files From Crowdin'
|
||||||
|
description: 'Deletes files from Crowdin that are no longer in the English curriculum folder'
|
||||||
|
runs:
|
||||||
|
using: 'node12'
|
||||||
|
main: './index.js'
|
46
tools/crowdin/actions/remove-deleted-files/index.js
Normal file
46
tools/crowdin/actions/remove-deleted-files/index.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
require('dotenv').config({ path: `${__dirname}/../../.env` });
|
||||||
|
// const core = require('@actions/core');
|
||||||
|
const util = require('util');
|
||||||
|
const exec = util.promisify(require('child_process').exec);
|
||||||
|
|
||||||
|
const { getFiles, deleteFile } = require('../../utils/files');
|
||||||
|
|
||||||
|
const getOutputFromCommand = async command => {
|
||||||
|
try {
|
||||||
|
const { stdout } = await exec(command);
|
||||||
|
return stdout;
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error');
|
||||||
|
console.log('command');
|
||||||
|
console.log(command + '\n');
|
||||||
|
console.log(err.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeDeletedFiles = async projectId => {
|
||||||
|
console.log('start deleting source files no longer in English curriculum...');
|
||||||
|
const crowdinFiles = await getFiles(projectId);
|
||||||
|
|
||||||
|
if (crowdinFiles && crowdinFiles.length) {
|
||||||
|
const command = 'find curriculum/challenges/english -name \\*.md';
|
||||||
|
const listOfEnglishFiles = await getOutputFromCommand(command);
|
||||||
|
const curriculumFilesArr = listOfEnglishFiles.split('\n');
|
||||||
|
if (curriculumFilesArr.length) {
|
||||||
|
const curriculumLookup = curriculumFilesArr.reduce((obj, filename) => {
|
||||||
|
return { ...obj, [filename]: 1 };
|
||||||
|
}, {});
|
||||||
|
for (let { fileId, path: crowdinFilePath } of crowdinFiles) {
|
||||||
|
if (!curriculumLookup.hasOwnProperty(crowdinFilePath)) {
|
||||||
|
await deleteFile(projectId, fileId, crowdinFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`WARNING! No Crowdin files found for projectId ${projectId}`);
|
||||||
|
}
|
||||||
|
console.log('deleting source non-existent source files complete');
|
||||||
|
};
|
||||||
|
|
||||||
|
const projectId = process.env.CROWDIN_PROJECT_ID;
|
||||||
|
removeDeletedFiles(projectId);
|
106
tools/crowdin/package-lock.json
generated
Normal file
106
tools/crowdin/package-lock.json
generated
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"name": "crowdin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"requires": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@actions/core": {
|
||||||
|
"version": "1.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
|
||||||
|
"integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
|
||||||
|
},
|
||||||
|
"argparse": {
|
||||||
|
"version": "1.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||||
|
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||||
|
"requires": {
|
||||||
|
"sprintf-js": "~1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dotenv": {
|
||||||
|
"version": "8.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
|
||||||
|
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
|
||||||
|
},
|
||||||
|
"esprima": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
|
||||||
|
},
|
||||||
|
"extend-shallow": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
|
||||||
|
"requires": {
|
||||||
|
"is-extendable": "^0.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gray-matter": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-7hB/+LxrOjq/dd8APlK0r24uL/67w7SkYnfwhNFwg/VDIGWGmduTDYf3WNstLW2fbbmRwrDGCVSJ2isuf2+4Hw==",
|
||||||
|
"requires": {
|
||||||
|
"js-yaml": "^3.11.0",
|
||||||
|
"kind-of": "^6.0.2",
|
||||||
|
"section-matter": "^1.0.0",
|
||||||
|
"strip-bom-string": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"is-extendable": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
||||||
|
"integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik="
|
||||||
|
},
|
||||||
|
"js-yaml": {
|
||||||
|
"version": "3.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||||
|
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||||
|
"requires": {
|
||||||
|
"argparse": "^1.0.7",
|
||||||
|
"esprima": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"kind-of": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
|
||||||
|
},
|
||||||
|
"node-fetch": {
|
||||||
|
"version": "2.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||||
|
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
|
||||||
|
},
|
||||||
|
"picomatch": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg=="
|
||||||
|
},
|
||||||
|
"readdirp": {
|
||||||
|
"version": "3.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
|
||||||
|
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
|
||||||
|
"requires": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"section-matter": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
|
||||||
|
"requires": {
|
||||||
|
"extend-shallow": "^2.0.1",
|
||||||
|
"kind-of": "^6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sprintf-js": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
|
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||||
|
},
|
||||||
|
"strip-bom-string": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
tools/crowdin/package.json
Normal file
18
tools/crowdin/package.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "crowdin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "freeCodeCamp",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@actions/core": "^1.2.6",
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
|
"gray-matter": "^4.0.2",
|
||||||
|
"node-fetch": "^2.6.1",
|
||||||
|
"readdirp": "^3.5.0"
|
||||||
|
}
|
||||||
|
}
|
7
tools/crowdin/utils/auth-header.js
Normal file
7
tools/crowdin/utils/auth-header.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const authHeader = {
|
||||||
|
Authorization: `Bearer ${process.env.CROWDIN_PERSONAL_TOKEN}`
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = authHeader;
|
5
tools/crowdin/utils/delay.js
Normal file
5
tools/crowdin/utils/delay.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const delay = (time = 2000) => {
|
||||||
|
return new Promise(resolve => setTimeout(() => resolve(true), time));
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = delay;
|
86
tools/crowdin/utils/dirs.js
Normal file
86
tools/crowdin/utils/dirs.js
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
const makeRequest = require('./make-request');
|
||||||
|
const delay = require('./delay');
|
||||||
|
const authHeader = require('./auth-header');
|
||||||
|
|
||||||
|
const getDirs = async projectId => {
|
||||||
|
let headers = { ...authHeader };
|
||||||
|
let done = false;
|
||||||
|
let offset = 0;
|
||||||
|
let files = [];
|
||||||
|
while (!done) {
|
||||||
|
const endPoint = `projects/${projectId}/directories?limit=500&offset=${offset}`;
|
||||||
|
await delay(1000);
|
||||||
|
const response = await makeRequest({
|
||||||
|
method: 'get',
|
||||||
|
endPoint,
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
if (response.data) {
|
||||||
|
if (response.data.length) {
|
||||||
|
files = [...files, ...response.data];
|
||||||
|
offset += 500;
|
||||||
|
} else {
|
||||||
|
done = true;
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const { error } = response;
|
||||||
|
console.log(error.errorcode);
|
||||||
|
console.log(error.messsage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addDir = async (projectId, dirName, parentDirId) => {
|
||||||
|
let headers = { ...authHeader };
|
||||||
|
const endPoint = `projects/${projectId}/directories`;
|
||||||
|
let body = {
|
||||||
|
name: dirName
|
||||||
|
};
|
||||||
|
if (parentDirId) {
|
||||||
|
body = { ...body, directoryId: parentDirId };
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await makeRequest({
|
||||||
|
method: 'post',
|
||||||
|
endPoint,
|
||||||
|
headers,
|
||||||
|
body
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDirs = async (crowdinDirs, dirPath) => {
|
||||||
|
// superParent is the top level directory on crowdin
|
||||||
|
const superParent = crowdinDirs.find(dir => !dir.data.directoryId);
|
||||||
|
let lastParentId = superParent.data.id;
|
||||||
|
|
||||||
|
const splitDirPath = dirPath.split('/');
|
||||||
|
splitDirPath.shift();
|
||||||
|
|
||||||
|
// we are assuming that the first directory in 'newFile' is the same as the superParent
|
||||||
|
// maybe throw a check in here to verify that's true
|
||||||
|
const findCurrDir = (directory, crowdinDirs) => {
|
||||||
|
return crowdinDirs.find(({ data: { name, directoryId } }) => {
|
||||||
|
return name === directory && directoryId === lastParentId;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let directory of splitDirPath) {
|
||||||
|
const currentDirectory = findCurrDir(directory, crowdinDirs);
|
||||||
|
if (!currentDirectory) {
|
||||||
|
const response = await addDir(10, directory, lastParentId);
|
||||||
|
lastParentId = response.data.id;
|
||||||
|
} else {
|
||||||
|
lastParentId = currentDirectory.data.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastParentId;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
addDir,
|
||||||
|
getDirs,
|
||||||
|
createDirs
|
||||||
|
};
|
124
tools/crowdin/utils/files.js
Normal file
124
tools/crowdin/utils/files.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
const makeRequest = require('./make-request');
|
||||||
|
const delay = require('./delay');
|
||||||
|
const authHeader = require('./auth-header');
|
||||||
|
|
||||||
|
const addFile = async (projectId, filename, fileContent, directoryId) => {
|
||||||
|
let headers = { ...authHeader };
|
||||||
|
headers['Crowdin-API-FileName'] = filename;
|
||||||
|
const endPoint = `storages`;
|
||||||
|
const contentType = 'application/text';
|
||||||
|
const body = fileContent;
|
||||||
|
const storageResponse = await makeRequest({
|
||||||
|
method: 'post',
|
||||||
|
contentType,
|
||||||
|
endPoint,
|
||||||
|
headers,
|
||||||
|
body
|
||||||
|
});
|
||||||
|
if (storageResponse.data) {
|
||||||
|
const fileBody = {
|
||||||
|
storageId: storageResponse.data.id,
|
||||||
|
name: filename,
|
||||||
|
directoryId
|
||||||
|
};
|
||||||
|
const fileResponse = await makeRequest({
|
||||||
|
method: 'post',
|
||||||
|
endPoint: `projects/${projectId}/files`,
|
||||||
|
headers,
|
||||||
|
body: fileBody
|
||||||
|
});
|
||||||
|
if (fileResponse.data) {
|
||||||
|
return fileResponse.data;
|
||||||
|
} else {
|
||||||
|
console.log('error');
|
||||||
|
console.dir(fileResponse, { depth: null, colors: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFile = async (projectId, fileId, fileContent) => {
|
||||||
|
let headers = { ...authHeader };
|
||||||
|
const endPoint = `storages`;
|
||||||
|
const contentType = 'application/text';
|
||||||
|
const body = fileContent;
|
||||||
|
const storageResponse = await makeRequest({
|
||||||
|
method: 'post',
|
||||||
|
contentType,
|
||||||
|
endPoint,
|
||||||
|
headers,
|
||||||
|
body
|
||||||
|
});
|
||||||
|
if (storageResponse.data) {
|
||||||
|
const fileBody = {
|
||||||
|
storageId: storageResponse.data.id
|
||||||
|
};
|
||||||
|
const fileResponse = await makeRequest({
|
||||||
|
method: 'put',
|
||||||
|
endPoint: `projects/${projectId}/files${fileId}`,
|
||||||
|
headers,
|
||||||
|
body: fileBody
|
||||||
|
});
|
||||||
|
if (fileResponse.data) {
|
||||||
|
return fileResponse.data;
|
||||||
|
} else {
|
||||||
|
console.log('error');
|
||||||
|
console.dir(fileResponse, { depth: null, colors: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteFile = async (projectId, fileId, filePath) => {
|
||||||
|
let headers = { ...authHeader };
|
||||||
|
const endPoint = `projects/${projectId}/files/${fileId}`;
|
||||||
|
await makeRequest({
|
||||||
|
method: 'delete',
|
||||||
|
endPoint,
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
console.log(`Deleted ${filePath} from Crowdin project`);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFiles = async projectId => {
|
||||||
|
let headers = { ...authHeader };
|
||||||
|
let done = false;
|
||||||
|
let offset = 0;
|
||||||
|
let files = [];
|
||||||
|
while (!done) {
|
||||||
|
const endPoint = `projects/${projectId}/files?limit=500&offset=${offset}`;
|
||||||
|
await delay(1000);
|
||||||
|
const response = await makeRequest({
|
||||||
|
method: 'get',
|
||||||
|
endPoint,
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
if (response.data) {
|
||||||
|
if (response.data.length) {
|
||||||
|
files = [...files, ...response.data];
|
||||||
|
offset += 500;
|
||||||
|
} else {
|
||||||
|
done = true;
|
||||||
|
files = files.map(({ data: { directoryId, id: fileId, path } }) => {
|
||||||
|
// remove leading forwardslash
|
||||||
|
path = path.slice(1);
|
||||||
|
return { directoryId, fileId, path };
|
||||||
|
});
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const { error } = response;
|
||||||
|
console.log(error.errorcode);
|
||||||
|
console.log(error.messsage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
addFile,
|
||||||
|
updateFile,
|
||||||
|
deleteFile,
|
||||||
|
getFiles
|
||||||
|
};
|
34
tools/crowdin/utils/make-request.js
Normal file
34
tools/crowdin/utils/make-request.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
const makeRequest = async ({
|
||||||
|
method,
|
||||||
|
endPoint,
|
||||||
|
contentType = 'application/json',
|
||||||
|
accept = 'application/json',
|
||||||
|
headers,
|
||||||
|
body
|
||||||
|
}) => {
|
||||||
|
headers = { ...headers, 'Content-Type': contentType, Accept: accept };
|
||||||
|
const apiUrl = process.env.CROWDIN_API_URL + endPoint;
|
||||||
|
|
||||||
|
if (contentType === 'application/x-www-form-urlencoded') {
|
||||||
|
body = Object.entries(body)
|
||||||
|
.reduce((formDataArr, [key, value]) => {
|
||||||
|
return formDataArr.concat(`${key}=${value}`);
|
||||||
|
}, [])
|
||||||
|
.join('&');
|
||||||
|
} else if (contentType === 'application/json') {
|
||||||
|
body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(apiUrl, { headers, method, body });
|
||||||
|
if (method !== 'delete') {
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = makeRequest;
|
74
tools/crowdin/utils/strings.js
Normal file
74
tools/crowdin/utils/strings.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
const authHeader = require('./auth-header');
|
||||||
|
const makeRequest = require('./make-request');
|
||||||
|
|
||||||
|
const isHeading = str => /\h\d/.test(str);
|
||||||
|
const isCode = str => /^\/pre\/code|\/code$/.test(str);
|
||||||
|
|
||||||
|
const shouldHide = (text, context, challengeTitle) => {
|
||||||
|
if (isHeading(context) || isCode(context)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return text !== challengeTitle && context.includes('id=front-matter');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStrings = async ({ projectId, fileId }) => {
|
||||||
|
let headers = { ...authHeader };
|
||||||
|
const endPoint = `projects/${projectId}/strings?fileId=${fileId}&limit=500`;
|
||||||
|
const strings = await makeRequest({ method: 'get', endPoint, headers });
|
||||||
|
if (strings.data) {
|
||||||
|
return strings.data;
|
||||||
|
} else {
|
||||||
|
const { error, errors } = strings;
|
||||||
|
console.error(error ? error : errors);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateString = async ({ projectId, stringId, propsToUpdate }) => {
|
||||||
|
let headers = { ...authHeader };
|
||||||
|
const endPoint = `projects/${projectId}/strings/${stringId}`;
|
||||||
|
const body = propsToUpdate.map(({ path, value }) => ({
|
||||||
|
op: 'replace',
|
||||||
|
path,
|
||||||
|
value
|
||||||
|
}));
|
||||||
|
await makeRequest({
|
||||||
|
method: 'patch',
|
||||||
|
endPoint,
|
||||||
|
headers,
|
||||||
|
body
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeHiddenStatus = async (projectId, stringId, newStatus) => {
|
||||||
|
await updateString({
|
||||||
|
projectId,
|
||||||
|
stringId,
|
||||||
|
propsToUpdate: [{ path: '/isHidden', value: newStatus }]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFileStrings = async ({ projectId, fileId, challengeTitle }) => {
|
||||||
|
const fileStrings = await getStrings({
|
||||||
|
projectId,
|
||||||
|
fileId
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let {
|
||||||
|
data: { id: stringId, text, isHidden, context }
|
||||||
|
} of fileStrings) {
|
||||||
|
const hideString = shouldHide(text, context, challengeTitle);
|
||||||
|
if (!isHidden && hideString) {
|
||||||
|
changeHiddenStatus(projectId, stringId, true);
|
||||||
|
} else if (isHidden && !hideString) {
|
||||||
|
changeHiddenStatus(projectId, stringId, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getStrings,
|
||||||
|
changeHiddenStatus,
|
||||||
|
updateString,
|
||||||
|
updateFileStrings
|
||||||
|
};
|
Reference in New Issue
Block a user