254 lines
7.7 KiB
TypeScript
Raw Normal View History

/* eslint-env node */
2020-01-17 12:44:22 +03:00
/* eslint-disable */
import fs from 'fs';
import path from 'path';
import axios from 'axios';
import JSON5 from 'json5';
import Crowdin, { SourceFilesModel } from '@crowdin/crowdin-api-client';
import ProgressBar from 'progress';
import ch from 'chalk';
import iso639 from 'iso-639-1';
import { prompt } from 'inquirer';
2020-01-17 12:44:22 +03:00
import config from './../../config';
2019-11-09 14:42:02 +02:00
if (!config.crowdin.apiKey) {
2020-05-24 02:08:24 +03:00
console.error(ch.red`crowdinApiKey is required`);
process.exit(126);
}
const PROJECT_ID = config.crowdin.projectId;
const CROWDIN_FILE_PATH = config.crowdin.filePath;
const SOURCE_LANG = config.crowdin.sourceLang;
const LANG_DIR = config.crowdin.basePath;
2019-12-07 13:28:52 +02:00
const INDEX_FILE_NAME = 'index.js';
const MIN_RELEASE_PROGRESS = config.crowdin.minApproved;
const crowdin = new Crowdin({
token: config.crowdin.apiKey,
2020-01-17 12:44:22 +03:00
});
/**
* Locales that has been verified by core team members
*/
const releasedLocales: ReadonlyArray<string> = ['be', 'fr', 'id', 'pt', 'ru', 'uk', 'vi', 'zh'];
/**
* Map Crowdin locales into our internal locales representation
*/
2020-01-17 12:44:22 +03:00
const LOCALES_MAP: Record<string, string> = {
2020-05-24 02:08:24 +03:00
'pt-BR': 'pt',
'zh-CN': 'zh',
};
/**
* This array allows us to customise native languages names,
* because ISO-639-1 sometimes is strange
*/
2020-01-17 12:44:22 +03:00
const NATIVE_NAMES_MAP: Record<string, string> = {
2020-05-24 02:08:24 +03:00
be: 'Беларуская',
id: 'Bahasa Indonesia',
lt: 'Lietuvių',
pl: 'Polski',
pt: 'Português do Brasil',
sr: 'Српски',
ro: 'Română',
zh: '简体中文',
};
/**
* This arrays allows us to override Crowdin English languages names
*/
2020-01-17 12:44:22 +03:00
const ENGLISH_NAMES_MAP: Record<string, string> = {
2020-05-24 02:08:24 +03:00
pt: 'Portuguese, Brazilian',
sr: 'Serbian',
zh: 'Simplified Chinese',
};
/**
* Converts Crowdin's language code to our internal value
*/
function toInternalLocale(code: string): string {
2020-05-24 02:08:24 +03:00
return LOCALES_MAP[code] || code;
}
/**
* Форматирует входящий объект с переводами в итоговую строку в том формате, в каком они
* хранятся в самом приложении
*/
2020-01-17 12:44:22 +03:00
function serializeToModule(translates: Record<string, any>): string {
const src = JSON5.stringify(sortByKeys(translates), null, 4);
2019-12-07 13:28:52 +02:00
2020-05-24 02:08:24 +03:00
return `module.exports = ${src};\n`;
}
2020-01-17 12:44:22 +03:00
// http://stackoverflow.com/a/29622653/5184751
function sortByKeys<T extends Record<string, any>>(object: T): T {
2020-05-24 02:08:24 +03:00
return Object.keys(object)
.sort()
.reduce((result, key) => {
// @ts-ignore
result[key] = object[key];
return result;
}, {} as T);
}
interface IndexFileEntry {
code: string;
name: string;
englishName: string;
progress: number;
isReleased: boolean;
}
function getLocaleFilePath(languageId: string): string {
return path.join(LANG_DIR, `${toInternalLocale(languageId)}.json`);
}
let directoriesList: Array<SourceFilesModel.Directory>;
let filesList: Array<SourceFilesModel.File>;
async function findFileId(path: string, parentDir: number|null = null): Promise<number> {
const [nodeToSearch, ...rest] = path.split('/');
if (rest.length === 0) {
if (!filesList) {
const { data: filesResponse } = await crowdin.sourceFilesApi.listProjectFiles(PROJECT_ID);
filesList = filesResponse.map((fileData) => fileData.data);
2020-05-24 02:08:24 +03:00
}
const file = filesList.find((file) => file.directoryId === parentDir && file.name === nodeToSearch);
if (file === undefined) {
throw new Error('Cannot find file by provided path');
2020-05-24 02:08:24 +03:00
}
return file.id;
2020-05-24 02:08:24 +03:00
}
if (!directoriesList) {
const { data: dirsResponse } = await crowdin.sourceFilesApi.listProjectDirectories(PROJECT_ID);
directoriesList = dirsResponse.map((dirData) => dirData.data);
}
const dir = directoriesList.find((dir) => dir.directoryId === parentDir && dir.name === nodeToSearch);
if (dir === undefined) {
throw new Error('Cannot find directory by provided path');
}
return findFileId(rest.join('/'), dir.id);
}
async function pull(): Promise<void> {
console.log('Loading file info...');
const fileId = await findFileId(CROWDIN_FILE_PATH);
console.log('Pulling translation progress...');
const { data: translationProgress } = await crowdin.translationStatusApi.getFileProgress(PROJECT_ID, fileId, 100);
const localesToPull: Array<string> = [];
const indexFileEntries: Record<string, IndexFileEntry> = {
en: {
code: 'en',
name: 'English',
englishName: 'English',
progress: 100,
isReleased: true,
},
};
translationProgress.forEach(({ data: { languageId, approvalProgress } }) => {
const locale = toInternalLocale(languageId);
if (releasedLocales.includes(locale) || approvalProgress >= MIN_RELEASE_PROGRESS) {
localesToPull.push(languageId);
indexFileEntries[locale] = {
code: locale,
name: NATIVE_NAMES_MAP[locale] || iso639.getNativeName(locale),
englishName: ENGLISH_NAMES_MAP[locale] || iso639.getName(locale),
progress: approvalProgress,
isReleased: releasedLocales.includes(locale),
};
}
});
// Add prefix 'c' to current and total to prevent filling thees placeholders with real values
const downloadingProgressBar = new ProgressBar('Downloading translates :bar :percent | :cCurrent/:total', {
total: localesToPull.length,
incomplete: '\u2591',
complete: '\u2588',
width: Object.keys(indexFileEntries).length - 1,
});
let downloadingReady = 0;
const promises = localesToPull.map(async (languageId): Promise<void> => {
const { data: { url } } = await crowdin.translationsApi.buildProjectFileTranslation(PROJECT_ID, fileId, {
targetLanguageId: languageId,
exportApprovedOnly: true,
});
const { data: fileContents } = await axios.get(url, {
// Disable response parsing
transformResponse: [],
});
fs.writeFileSync(getLocaleFilePath(languageId), fileContents);
downloadingProgressBar.update(++downloadingReady / localesToPull.length, {
cCurrent: downloadingReady,
});
});
await Promise.all(promises);
console.log('Writing an index file');
fs.writeFileSync(path.join(LANG_DIR, INDEX_FILE_NAME), serializeToModule(indexFileEntries));
console.log(ch.green('The index file was successfully written'));
}
async function push(): Promise<void> {
const { disapproveTranslates } = await prompt([{
name: 'disapproveTranslates',
type: 'confirm',
default: true,
message: 'Disapprove changed lines?',
}]);
console.log('Loading file info...');
const fileId = await findFileId(CROWDIN_FILE_PATH);
console.log('Uploading the source file to the storage...')
const { data: { id: storageId } } = await crowdin.uploadStorageApi.addStorage(
path.basename(CROWDIN_FILE_PATH),
fs.readFileSync(getLocaleFilePath(SOURCE_LANG)),
2020-05-24 02:08:24 +03:00
);
console.log(`Applying the new revision...`);
await crowdin.sourceFilesApi.updateOrRestoreFile(PROJECT_ID, fileId, {
storageId,
updateOption: disapproveTranslates
? SourceFilesModel.UpdateOption.CLEAR_TRANSLATIONS_AND_APPROVALS
: SourceFilesModel.UpdateOption.KEEP_TRANSLATIONS_AND_APPROVALS,
2020-05-24 02:08:24 +03:00
});
console.log(ch.green('Success'));
}
try {
2020-05-24 02:08:24 +03:00
const action = process.argv[2];
switch (action) {
case 'pull':
pull();
2020-05-24 02:08:24 +03:00
break;
case 'push':
push();
break;
default:
console.error(`Unknown action ${action}`);
}
} catch (exception) {
2020-05-24 02:08:24 +03:00
console.error(exception);
}