2019-03-09 05:40:06 +05:30
|
|
|
|
/* eslint-env node */
|
2020-01-17 15:14:22 +05:30
|
|
|
|
/* eslint-disable */
|
2019-03-09 05:40:06 +05:30
|
|
|
|
|
|
|
|
|
import fs from 'fs';
|
|
|
|
|
import path from 'path';
|
2020-05-20 22:05:52 +05:30
|
|
|
|
import CrowdinApi, {
|
|
|
|
|
LanguageStatusNode,
|
|
|
|
|
LanguageStatusResponse,
|
|
|
|
|
ProjectInfoResponse,
|
|
|
|
|
} from 'crowdin-api';
|
2019-03-09 05:40:06 +05:30
|
|
|
|
import MultiProgress from 'multi-progress';
|
|
|
|
|
import ch from 'chalk';
|
|
|
|
|
import iso639 from 'iso-639-1';
|
|
|
|
|
import prompt from 'prompt';
|
|
|
|
|
|
2020-01-17 15:14:22 +05:30
|
|
|
|
import { ValuesType } from 'utility-types';
|
|
|
|
|
|
2019-12-08 00:32:00 +05:30
|
|
|
|
import config from '../../config';
|
2019-11-09 18:12:02 +05:30
|
|
|
|
|
|
|
|
|
if (!config.crowdinApiKey) {
|
2019-11-27 14:33:32 +05:30
|
|
|
|
console.error(ch.red`crowdinApiKey is required`);
|
|
|
|
|
process.exit(126);
|
2019-03-09 05:40:06 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const PROJECT_ID = 'elyby';
|
2019-11-09 18:12:02 +05:30
|
|
|
|
const PROJECT_KEY = config.crowdinApiKey;
|
2019-03-09 05:40:06 +05:30
|
|
|
|
const CROWDIN_FILE_PATH = 'accounts/site.json';
|
|
|
|
|
const SOURCE_LANG = 'en';
|
2019-12-08 00:32:00 +05:30
|
|
|
|
const LANG_DIR = path.resolve(`${__dirname}/../app/i18n`);
|
2019-12-07 16:58:52 +05:30
|
|
|
|
const INDEX_FILE_NAME = 'index.js';
|
2019-03-09 05:40:06 +05:30
|
|
|
|
const MIN_RELEASE_PROGRESS = 80; // Minimal ready percent before translation can be published
|
|
|
|
|
|
2020-01-17 15:14:22 +05:30
|
|
|
|
const crowdin = new CrowdinApi({
|
|
|
|
|
apiKey: PROJECT_KEY,
|
|
|
|
|
projectName: PROJECT_ID,
|
|
|
|
|
});
|
2019-03-09 05:40:06 +05:30
|
|
|
|
const progressBar = new MultiProgress();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Locales that has been verified by core team members
|
|
|
|
|
*/
|
2020-05-20 22:05:52 +05:30
|
|
|
|
const RELEASED_LOCALES: Array<string> = [
|
|
|
|
|
'be',
|
|
|
|
|
'fr',
|
|
|
|
|
'id',
|
|
|
|
|
'pt',
|
|
|
|
|
'ru',
|
|
|
|
|
'uk',
|
|
|
|
|
'vi',
|
|
|
|
|
'zh',
|
|
|
|
|
];
|
2019-03-09 05:40:06 +05:30
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Array of Crowdin locales to our internal locales representation
|
|
|
|
|
*/
|
2020-01-17 15:14:22 +05:30
|
|
|
|
const LOCALES_MAP: Record<string, string> = {
|
2019-11-27 14:33:32 +05:30
|
|
|
|
'pt-BR': 'pt',
|
|
|
|
|
'zh-CN': 'zh',
|
2019-03-09 05:40:06 +05:30
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* This array allows us to customise native languages names, because ISO-639-1 sometimes is strange
|
|
|
|
|
*/
|
2020-01-17 15:14:22 +05:30
|
|
|
|
const NATIVE_NAMES_MAP: Record<string, string> = {
|
2019-11-27 14:33:32 +05:30
|
|
|
|
be: 'Беларуская',
|
|
|
|
|
id: 'Bahasa Indonesia',
|
|
|
|
|
lt: 'Lietuvių',
|
|
|
|
|
pl: 'Polski',
|
|
|
|
|
pt: 'Português do Brasil',
|
|
|
|
|
sr: 'Српски',
|
|
|
|
|
ro: 'Română',
|
|
|
|
|
zh: '简体中文',
|
2019-03-09 05:40:06 +05:30
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* This arrays allows us to override Crowdin English languages names
|
|
|
|
|
*/
|
2020-01-17 15:14:22 +05:30
|
|
|
|
const ENGLISH_NAMES_MAP: Record<string, string> = {
|
2019-11-27 14:33:32 +05:30
|
|
|
|
pt: 'Portuguese, Brazilian',
|
|
|
|
|
sr: 'Serbian',
|
|
|
|
|
zh: 'Simplified Chinese',
|
2019-03-09 05:40:06 +05:30
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Converts Crowdin's language code to our internal value
|
|
|
|
|
*/
|
|
|
|
|
function toInternalLocale(code: string): string {
|
2019-11-27 14:33:32 +05:30
|
|
|
|
return LOCALES_MAP[code] || code;
|
2019-03-09 05:40:06 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Форматирует входящий объект с переводами в итоговую строку в том формате, в каком они
|
|
|
|
|
* хранятся в самом приложении
|
|
|
|
|
*/
|
2020-01-17 15:14:22 +05:30
|
|
|
|
function serializeToModule(translates: Record<string, any>): string {
|
2019-12-07 16:58:52 +05:30
|
|
|
|
const src = JSON.stringify(sortByKeys(translates), null, 2);
|
|
|
|
|
|
2020-01-17 15:14:22 +05:30
|
|
|
|
return `module.exports = ${src};\n`;
|
2019-03-09 05:40:06 +05:30
|
|
|
|
}
|
|
|
|
|
|
2020-01-17 15:14:22 +05:30
|
|
|
|
// http://stackoverflow.com/a/29622653/5184751
|
|
|
|
|
function sortByKeys<T extends Record<string, any>>(object: T): T {
|
2019-11-27 14:33:32 +05:30
|
|
|
|
return Object.keys(object)
|
|
|
|
|
.sort()
|
|
|
|
|
.reduce((result, key) => {
|
2020-01-17 15:14:22 +05:30
|
|
|
|
// @ts-ignore
|
2019-11-27 14:33:32 +05:30
|
|
|
|
result[key] = object[key];
|
|
|
|
|
|
|
|
|
|
return result;
|
2020-01-17 15:14:22 +05:30
|
|
|
|
}, {} as T);
|
2019-03-09 05:40:06 +05:30
|
|
|
|
}
|
|
|
|
|
|
2020-01-17 15:14:22 +05:30
|
|
|
|
async function pullLocales(): Promise<ProjectInfoResponse['languages']> {
|
|
|
|
|
const { languages } = await crowdin.projectInfo();
|
2019-11-27 14:33:32 +05:30
|
|
|
|
|
|
|
|
|
return languages;
|
2019-03-09 05:40:06 +05:30
|
|
|
|
}
|
|
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
|
function findFile(
|
2020-01-17 15:14:22 +05:30
|
|
|
|
root: LanguageStatusResponse['files'],
|
2019-11-27 14:33:32 +05:30
|
|
|
|
path: string,
|
|
|
|
|
): LanguageStatusNode | null {
|
|
|
|
|
const [nodeToSearch, ...rest] = path.split('/');
|
2019-03-09 05:40:06 +05:30
|
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
|
for (const node of root) {
|
|
|
|
|
if (node.name !== nodeToSearch) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2019-03-09 05:40:06 +05:30
|
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
|
if (rest.length === 0) {
|
|
|
|
|
return node;
|
2019-03-09 05:40:06 +05:30
|
|
|
|
}
|
|
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
|
return findFile(node.files, rest.join('/'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
2019-03-09 05:40:06 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface IndexFileEntry {
|
2019-11-27 14:33:32 +05:30
|
|
|
|
code: string;
|
|
|
|
|
name: string;
|
|
|
|
|
englishName: string;
|
|
|
|
|
progress: number;
|
|
|
|
|
isReleased: boolean;
|
2019-03-09 05:40:06 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function pull() {
|
2019-11-27 14:33:32 +05:30
|
|
|
|
console.log('Pulling locales list...');
|
|
|
|
|
const locales = await pullLocales();
|
|
|
|
|
const checkingProgressBar = progressBar.newBar(
|
|
|
|
|
'| Pulling locales info :bar :percent | :current/:total',
|
|
|
|
|
{
|
|
|
|
|
total: locales.length,
|
|
|
|
|
incomplete: '\u2591',
|
|
|
|
|
complete: '\u2588',
|
|
|
|
|
width: locales.length,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
// Add prefix 'c' to current and total to prevent filling thees placeholders with real values
|
|
|
|
|
const downloadingProgressBar = progressBar.newBar(
|
|
|
|
|
'| Downloading translates :bar :percent | :cCurrent/:cTotal',
|
|
|
|
|
{
|
|
|
|
|
total: 100,
|
|
|
|
|
incomplete: '\u2591',
|
|
|
|
|
complete: '\u2588',
|
|
|
|
|
width: locales.length,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
let downloadingTotal = 0;
|
|
|
|
|
let downloadingReady = 0;
|
2020-01-17 15:14:22 +05:30
|
|
|
|
|
|
|
|
|
interface Result {
|
2020-05-20 22:05:52 +05:30
|
|
|
|
locale: ValuesType<typeof locales>;
|
|
|
|
|
progress: number;
|
|
|
|
|
translatesFilePath: string;
|
2020-01-17 15:14:22 +05:30
|
|
|
|
}
|
|
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
|
const results = await Promise.all(
|
2020-01-17 15:14:22 +05:30
|
|
|
|
// TODO: there is should be some way to reimplement this
|
|
|
|
|
// with reduce to avoid null values
|
2020-05-20 22:05:52 +05:30
|
|
|
|
locales.map(
|
|
|
|
|
async (locale): Promise<Result | null> => {
|
|
|
|
|
const { files } = await crowdin.languageStatus(locale.code);
|
|
|
|
|
checkingProgressBar.tick();
|
|
|
|
|
const fileInfo = findFile(files, CROWDIN_FILE_PATH);
|
|
|
|
|
|
|
|
|
|
if (fileInfo === null) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
'Unable to find translation file. Please check the CROWDIN_FILE_PATH param.',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const progress = (fileInfo.words_approved / fileInfo.words) * 100;
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!RELEASED_LOCALES.includes(toInternalLocale(locale.code)) &&
|
|
|
|
|
progress < MIN_RELEASE_PROGRESS
|
|
|
|
|
) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
downloadingProgressBar.update(downloadingReady / ++downloadingTotal, {
|
|
|
|
|
cCurrent: downloadingReady,
|
|
|
|
|
cTotal: downloadingTotal,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const translatesFilePath = await crowdin.exportFile(
|
|
|
|
|
CROWDIN_FILE_PATH,
|
|
|
|
|
locale.code,
|
2019-11-27 14:33:32 +05:30
|
|
|
|
);
|
2020-05-20 22:05:52 +05:30
|
|
|
|
|
|
|
|
|
downloadingProgressBar.update(++downloadingReady / downloadingTotal, {
|
|
|
|
|
cCurrent: downloadingReady,
|
|
|
|
|
cTotal: downloadingTotal,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
locale,
|
|
|
|
|
progress,
|
|
|
|
|
translatesFilePath,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
),
|
2019-11-27 14:33:32 +05:30
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
console.log('Locales are downloaded. Writing them to file system.');
|
|
|
|
|
|
2019-12-07 16:58:52 +05:30
|
|
|
|
const indexFileEntries: { [key: string]: IndexFileEntry } = {
|
2019-11-27 14:33:32 +05:30
|
|
|
|
en: {
|
|
|
|
|
code: 'en',
|
|
|
|
|
name: 'English',
|
|
|
|
|
englishName: 'English',
|
|
|
|
|
progress: 100,
|
|
|
|
|
isReleased: true,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
await Promise.all(
|
2020-01-17 15:14:22 +05:30
|
|
|
|
results
|
|
|
|
|
.filter((result): result is Result => result !== null)
|
2020-05-20 22:05:52 +05:30
|
|
|
|
.map(
|
|
|
|
|
(result) =>
|
|
|
|
|
new Promise((resolve, reject) => {
|
|
|
|
|
const {
|
|
|
|
|
locale: { code, name },
|
|
|
|
|
progress,
|
|
|
|
|
translatesFilePath,
|
|
|
|
|
} = result;
|
|
|
|
|
const ourCode = toInternalLocale(code);
|
|
|
|
|
|
|
|
|
|
indexFileEntries[ourCode] = {
|
|
|
|
|
code: ourCode,
|
|
|
|
|
name: NATIVE_NAMES_MAP[ourCode] || iso639.getNativeName(ourCode),
|
|
|
|
|
englishName: ENGLISH_NAMES_MAP[ourCode] || name,
|
|
|
|
|
progress: parseFloat(progress.toFixed(1)),
|
|
|
|
|
isReleased: RELEASED_LOCALES.includes(ourCode),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fs.copyFile(
|
|
|
|
|
translatesFilePath,
|
|
|
|
|
path.join(LANG_DIR, `${ourCode}.json`),
|
|
|
|
|
0,
|
|
|
|
|
(err) => {
|
|
|
|
|
err ? reject(err) : resolve();
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
),
|
2019-11-27 14:33:32 +05:30
|
|
|
|
);
|
2019-03-09 05:40:06 +05:30
|
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
|
console.log('Writing an index file.');
|
2019-03-09 05:40:06 +05:30
|
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
path.join(LANG_DIR, INDEX_FILE_NAME),
|
2020-01-17 15:14:22 +05:30
|
|
|
|
serializeToModule(indexFileEntries),
|
2019-11-27 14:33:32 +05:30
|
|
|
|
);
|
2019-03-09 05:40:06 +05:30
|
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
|
console.log(ch.green('The index file was successfully written'));
|
2019-03-09 05:40:06 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function push() {
|
2019-11-27 14:33:32 +05:30
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
prompt.start();
|
|
|
|
|
prompt.get(
|
|
|
|
|
{
|
|
|
|
|
properties: {
|
|
|
|
|
disapprove: {
|
|
|
|
|
description: 'Disapprove changed lines? [Y/n]',
|
|
|
|
|
pattern: /^y|n$/i,
|
|
|
|
|
message: 'Please enter "y" or "n"',
|
|
|
|
|
default: 'y',
|
2020-05-20 22:05:52 +05:30
|
|
|
|
before: (value) => value.toLowerCase() === 'y',
|
2019-11-27 14:33:32 +05:30
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
async (err, { disapprove }) => {
|
|
|
|
|
if (err) {
|
|
|
|
|
reject(err);
|
2019-03-09 05:40:06 +05:30
|
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
|
return;
|
|
|
|
|
}
|
2019-03-09 05:40:06 +05:30
|
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
|
console.log(`Publishing ${ch.bold(SOURCE_LANG)} translates file...`);
|
|
|
|
|
|
|
|
|
|
await crowdin.updateFile(
|
|
|
|
|
{
|
|
|
|
|
[CROWDIN_FILE_PATH]: path.join(LANG_DIR, `${SOURCE_LANG}.json`),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
update_option: disapprove
|
|
|
|
|
? 'update_as_unapproved'
|
|
|
|
|
: 'update_without_changes',
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
console.log(ch.green('Success'));
|
|
|
|
|
|
|
|
|
|
resolve();
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
});
|
2019-03-09 05:40:06 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2019-11-27 14:33:32 +05:30
|
|
|
|
const action = process.argv[2];
|
|
|
|
|
|
|
|
|
|
switch (action) {
|
|
|
|
|
case 'pull':
|
|
|
|
|
pull();
|
|
|
|
|
break;
|
|
|
|
|
case 'push':
|
|
|
|
|
push();
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
console.error(`Unknown action ${action}`);
|
|
|
|
|
}
|
2019-03-09 05:40:06 +05:30
|
|
|
|
} catch (exception) {
|
2019-11-27 14:33:32 +05:30
|
|
|
|
console.error(exception);
|
2019-03-09 05:40:06 +05:30
|
|
|
|
}
|