mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-11-30 02:32:58 +05:30
930a272cd0
Show crowdin's error at any depth
Show branch name on i18n:push
Unpack git .pack objects to fix automatic branch detect (seems like a workaround for a workaround 🙃)
Detect git branch from CI if available.
Replace git-repo-info with git-rev-sync (maintained)
Fix accidentally removed git installation for Yarn job
422 lines
13 KiB
TypeScript
422 lines
13 KiB
TypeScript
/* eslint-disable jsdoc/require-param */
|
|
/* eslint-env node */
|
|
|
|
import chalk from 'chalk';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import util from 'util';
|
|
|
|
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, DistinctQuestion } from 'inquirer';
|
|
|
|
import git from 'git-rev-sync';
|
|
|
|
import { crowdin as config } from './../../config';
|
|
|
|
if (!config.apiKey) {
|
|
console.error(ch.red`crowdinApiKey is required`);
|
|
process.exit(126);
|
|
}
|
|
|
|
const PROJECT_ID = config.projectId;
|
|
const CROWDIN_FILE_PATH = config.filePath;
|
|
const SOURCE_LANG = config.sourceLang;
|
|
const LANG_DIR = config.basePath;
|
|
const INDEX_FILE_NAME = 'index.js';
|
|
const MIN_RELEASE_PROGRESS = config.minApproved;
|
|
|
|
const crowdin = new Crowdin({
|
|
token: config.apiKey,
|
|
});
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
const LOCALES_MAP: Record<string, string> = {
|
|
'pt-BR': 'pt',
|
|
'zh-CN': 'zh',
|
|
};
|
|
|
|
/**
|
|
* This array allows us to customise native languages names,
|
|
* because ISO-639-1 sometimes is strange
|
|
*/
|
|
const NATIVE_NAMES_MAP: Record<string, string> = {
|
|
be: 'Беларуская',
|
|
cs: 'Čeština',
|
|
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
|
|
*/
|
|
const ENGLISH_NAMES_MAP: Record<string, string> = {
|
|
pt: 'Portuguese, Brazilian',
|
|
sr: 'Serbian',
|
|
zh: 'Simplified Chinese',
|
|
};
|
|
|
|
/**
|
|
* Converts Crowdin's language code to our internal value
|
|
*/
|
|
function toInternalLocale(code: string): string {
|
|
return LOCALES_MAP[code] || code;
|
|
}
|
|
|
|
/**
|
|
* Formats the incoming hash with the translations in the module string in the format
|
|
* as they are stored in the application itself
|
|
*/
|
|
function serializeToModule(translates: Record<string, any>): string {
|
|
const src = JSON5.stringify(sortByKeys(translates), null, 4);
|
|
|
|
return `export default ${src};\n`;
|
|
}
|
|
|
|
// http://stackoverflow.com/a/29622653/5184751
|
|
function sortByKeys<T extends Record<string, any>>(object: T): T {
|
|
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`);
|
|
}
|
|
|
|
async function findDirectoryId(directoryPath: string, branchId?: number): Promise<number | undefined> {
|
|
const { data: dirsResponse } = await crowdin.sourceFilesApi.listProjectDirectories(PROJECT_ID, branchId);
|
|
const dirs = dirsResponse.map((dirData) => dirData.data);
|
|
|
|
const result = directoryPath.split('/').reduce((parentDir, dirName) => {
|
|
// directoryId is nullable when a directory has no parent
|
|
return dirs.find((dir) => dir.directoryId === parentDir && dir.name === dirName)?.id;
|
|
}, null as number | null | undefined);
|
|
|
|
return result || undefined;
|
|
}
|
|
|
|
async function findFileId(filePath: string, branchId?: number): Promise<number | undefined> {
|
|
const fileName = path.basename(filePath);
|
|
const dirPath = path.dirname(filePath);
|
|
let directoryId: number | null = null;
|
|
|
|
if (dirPath !== '') {
|
|
directoryId = (await findDirectoryId(dirPath, branchId)) || null;
|
|
}
|
|
|
|
// When the branchId and directoryId aren't specified the list returned recursively,
|
|
// but if one of this param is specified, the list contains only the specified parent.
|
|
//
|
|
// Directories ids for each branch are different, so directoryId is already ensures
|
|
// that the file will be searched in the correct branch
|
|
const { data: filesResponse } = await crowdin.sourceFilesApi.listProjectFiles(
|
|
PROJECT_ID,
|
|
directoryId === null ? branchId : undefined,
|
|
directoryId || undefined, // directory ID can't be 0, but can be null
|
|
);
|
|
const files = filesResponse.map((fileData) => fileData.data);
|
|
|
|
// Compare directoryId since when the file is placed in the root
|
|
return files.find((file) => file.directoryId === directoryId && file.name === fileName)?.id;
|
|
}
|
|
|
|
async function findBranchId(branchName: string): Promise<number | undefined> {
|
|
const { data: branchesList } = await crowdin.sourceFilesApi.listProjectBranches(PROJECT_ID, branchName);
|
|
const branchData = branchesList.find(({ data: branch }) => branch.name === branchName);
|
|
|
|
return branchData?.data.id;
|
|
}
|
|
|
|
async function ensureDirectory(dirPath: string, branchId?: number): Promise<number> {
|
|
const { data: dirsResponse } = await crowdin.sourceFilesApi.listProjectDirectories(PROJECT_ID, branchId);
|
|
const dirs = dirsResponse.map((dirData) => dirData.data);
|
|
|
|
return dirPath.split('/').reduce(async (parentDirPromise, name) => {
|
|
const parentDir = await parentDirPromise;
|
|
const directoryId = dirs.find((dir) => dir.directoryId === parentDir && dir.name === name)?.id;
|
|
|
|
if (directoryId) {
|
|
return directoryId;
|
|
}
|
|
|
|
const createDirRequest: SourceFilesModel.CreateDirectoryRequest = { name };
|
|
|
|
if (directoryId) {
|
|
createDirRequest['directoryId'] = directoryId;
|
|
} else if (branchId) {
|
|
createDirRequest['branchId'] = branchId;
|
|
}
|
|
|
|
const dirResponse = await crowdin.sourceFilesApi.createDirectory(PROJECT_ID, createDirRequest);
|
|
|
|
return dirResponse.data.id;
|
|
// @ts-ignore
|
|
}, Promise.resolve<number>(null));
|
|
}
|
|
|
|
function getGitBranch(): string {
|
|
if (process.env.CI_COMMIT_BRANCH) {
|
|
return process.env.CI_COMMIT_BRANCH;
|
|
}
|
|
|
|
const branch = git.branch();
|
|
|
|
if (branch.startsWith('Detached')) {
|
|
throw new Error(`Cannot automatically detect git branch. Received "${branch}"`);
|
|
}
|
|
|
|
return branch;
|
|
}
|
|
|
|
async function pull(): Promise<void> {
|
|
const branchName = getGitBranch();
|
|
const isMasterBranch = branchName === 'master';
|
|
let branchId: number | undefined;
|
|
|
|
if (!isMasterBranch) {
|
|
console.log(
|
|
[
|
|
`Current branch isn't ${chalk.green('master')},`,
|
|
`will try to pull translates from the ${chalk.green(branchName)} branch`,
|
|
].join(' '),
|
|
);
|
|
branchId = await findBranchId(branchName);
|
|
|
|
if (!branchId) {
|
|
console.log(`Branch ${chalk.green(branchName)} isn't found, will use ${chalk.green('master')} instead`);
|
|
}
|
|
}
|
|
|
|
console.log('Loading file info...');
|
|
const fileId = await findFileId(CROWDIN_FILE_PATH, branchId);
|
|
|
|
if (!fileId) {
|
|
throw new Error('Cannot find the file');
|
|
}
|
|
|
|
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> {
|
|
if (!fs.existsSync(getLocaleFilePath(SOURCE_LANG))) {
|
|
console.error(
|
|
chalk.red(
|
|
`File for the source language doesn't exists. Run ${chalk.green(
|
|
'yarn i18n:extract',
|
|
)} to generate the source language file.`,
|
|
),
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
const questions: Array<DistinctQuestion> = [
|
|
{
|
|
name: 'disapproveTranslates',
|
|
type: 'confirm',
|
|
default: true,
|
|
message: 'Disapprove changed lines?',
|
|
},
|
|
];
|
|
|
|
const branchName = getGitBranch();
|
|
const isMasterBranch = branchName === 'master';
|
|
|
|
if (!isMasterBranch) {
|
|
questions.push({
|
|
name: 'publishInBranch',
|
|
type: 'confirm',
|
|
default: true,
|
|
message: `Should be strings published in its own branch [${chalk.green(branchName)}]?`,
|
|
});
|
|
}
|
|
|
|
let disapproveTranslates = true;
|
|
let publishInBranch = !isMasterBranch;
|
|
try {
|
|
const answers = await prompt(questions);
|
|
disapproveTranslates = answers[0]; // eslint-disable-line prefer-destructuring
|
|
publishInBranch = answers[1] || false;
|
|
} catch (err) {
|
|
// okay if it's a tty error
|
|
if (!err.isTtyError) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
let branchId: number | undefined;
|
|
|
|
if (publishInBranch) {
|
|
console.log('Loading the branch info...');
|
|
branchId = await findBranchId(branchName);
|
|
|
|
if (!branchId) {
|
|
console.log("Branch doesn't exists. Creating...");
|
|
const { data: branchResponse } = await crowdin.sourceFilesApi.createBranch(PROJECT_ID, {
|
|
name: branchName,
|
|
});
|
|
branchId = branchResponse.id;
|
|
}
|
|
}
|
|
|
|
console.log('Loading the file info...');
|
|
const fileId = await findFileId(CROWDIN_FILE_PATH, branchId);
|
|
let dirId: number | undefined;
|
|
|
|
if (!fileId) {
|
|
const dirPath = path.dirname(CROWDIN_FILE_PATH);
|
|
|
|
if (dirPath !== '') {
|
|
console.log('Ensuring necessary directories structure...');
|
|
dirId = await ensureDirectory(dirPath, branchId);
|
|
}
|
|
}
|
|
|
|
console.log('Uploading the source file to the storage...');
|
|
const { data: storageResponse } = await crowdin.uploadStorageApi.addStorage(
|
|
path.basename(CROWDIN_FILE_PATH),
|
|
fs.readFileSync(getLocaleFilePath(SOURCE_LANG)),
|
|
);
|
|
|
|
if (fileId) {
|
|
console.log(`Applying the new revision...`);
|
|
await crowdin.sourceFilesApi.updateOrRestoreFile(PROJECT_ID, fileId, {
|
|
storageId: storageResponse.id,
|
|
updateOption: disapproveTranslates
|
|
? SourceFilesModel.UpdateOption.CLEAR_TRANSLATIONS_AND_APPROVALS
|
|
: SourceFilesModel.UpdateOption.KEEP_TRANSLATIONS_AND_APPROVALS,
|
|
});
|
|
} else {
|
|
console.log(`Uploading the file...`);
|
|
const createFileRequest: SourceFilesModel.CreateFileRequest = {
|
|
storageId: storageResponse.id,
|
|
name: path.basename(CROWDIN_FILE_PATH),
|
|
};
|
|
|
|
if (dirId) {
|
|
createFileRequest['directoryId'] = dirId;
|
|
} else if (branchId) {
|
|
createFileRequest['branchId'] = branchId;
|
|
}
|
|
|
|
await crowdin.sourceFilesApi.createFile(PROJECT_ID, createFileRequest);
|
|
}
|
|
|
|
console.log(ch.green('Success'));
|
|
}
|
|
|
|
(async () => {
|
|
try {
|
|
const action = process.argv[2]; // eslint-disable-line prefer-destructuring
|
|
|
|
switch (action) {
|
|
case 'pull':
|
|
await pull();
|
|
break;
|
|
case 'push':
|
|
await push();
|
|
break;
|
|
default:
|
|
console.error(`Unknown action ${action}`);
|
|
}
|
|
} catch (exception) {
|
|
console.error(util.inspect(exception, { depth: null }));
|
|
process.exit(1);
|
|
}
|
|
})();
|