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
2020-06-05 22:11:42 +05:30
import chalk from 'chalk' ;
2019-03-09 05:40:06 +05:30
import fs from 'fs' ;
import path from 'path' ;
2020-05-27 15:37:25 +05:30
2020-05-26 21:52:21 +05:30
import axios from 'axios' ;
import JSON5 from 'json5' ;
2020-05-27 15:37:25 +05:30
import Crowdin , { SourceFilesModel } from '@crowdin/crowdin-api-client' ;
import ProgressBar from 'progress' ;
2019-03-09 05:40:06 +05:30
import ch from 'chalk' ;
import iso639 from 'iso-639-1' ;
2020-06-05 22:11:42 +05:30
import { prompt , DistinctQuestion } from 'inquirer' ;
2020-01-17 15:14:22 +05:30
2020-06-05 22:11:42 +05:30
import getRepoInfo from 'git-repo-info' ;
2019-11-09 18:12:02 +05:30
2020-06-05 22:11:42 +05:30
import { crowdin as config } from './../../config' ;
if ( ! config . apiKey ) {
2020-05-24 04:38:24 +05:30
console . error ( ch . red ` crowdinApiKey is required ` ) ;
process . exit ( 126 ) ;
2019-03-09 05:40:06 +05:30
}
2020-06-05 22:11:42 +05:30
const PROJECT_ID = config . projectId ;
const CROWDIN_FILE_PATH = config . filePath ;
const SOURCE_LANG = config . sourceLang ;
const LANG_DIR = config . basePath ;
2019-12-07 16:58:52 +05:30
const INDEX_FILE_NAME = 'index.js' ;
2020-06-05 22:11:42 +05:30
const MIN_RELEASE_PROGRESS = config . minApproved ;
2020-05-26 21:52:21 +05:30
2020-05-27 15:37:25 +05:30
const crowdin = new Crowdin ( {
2020-06-05 22:11:42 +05:30
token : config.apiKey ,
2020-01-17 15:14:22 +05:30
} ) ;
2019-03-09 05:40:06 +05:30
/ * *
* Locales that has been verified by core team members
* /
2020-05-27 15:37:25 +05:30
const releasedLocales : ReadonlyArray < string > = [ 'be' , 'fr' , 'id' , 'pt' , 'ru' , 'uk' , 'vi' , 'zh' ] ;
2019-03-09 05:40:06 +05:30
/ * *
2020-05-27 15:37:25 +05:30
* Map Crowdin locales into our internal locales representation
2019-03-09 05:40:06 +05:30
* /
2020-01-17 15:14:22 +05:30
const LOCALES_MAP : Record < string , string > = {
2020-05-24 04:38:24 +05:30
'pt-BR' : 'pt' ,
'zh-CN' : 'zh' ,
2019-03-09 05:40:06 +05:30
} ;
/ * *
2020-05-27 15:37:25 +05:30
* This array allows us to customise native languages names ,
* because ISO - 639 - 1 sometimes is strange
2019-03-09 05:40:06 +05:30
* /
2020-01-17 15:14:22 +05:30
const NATIVE_NAMES_MAP : Record < string , string > = {
2020-05-24 04:38:24 +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 > = {
2020-05-24 04:38:24 +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 {
2020-05-24 04:38:24 +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 {
2020-05-26 21:52:21 +05:30
const src = JSON5 . stringify ( sortByKeys ( translates ) , null , 4 ) ;
2019-12-07 16:58:52 +05:30
2020-06-05 22:11:42 +05:30
return ` export default ${ 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 {
2020-05-24 04:38:24 +05:30
return Object . keys ( object )
. sort ( )
. reduce ( ( result , key ) = > {
// @ts-ignore
result [ key ] = object [ key ] ;
return result ;
} , { } as T ) ;
2019-03-09 05:40:06 +05:30
}
2020-05-27 15:37:25 +05:30
interface IndexFileEntry {
code : string ;
name : string ;
englishName : string ;
progress : number ;
isReleased : boolean ;
}
2019-11-27 14:33:32 +05:30
2020-05-27 15:37:25 +05:30
function getLocaleFilePath ( languageId : string ) : string {
return path . join ( LANG_DIR , ` ${ toInternalLocale ( languageId ) } .json ` ) ;
2019-03-09 05:40:06 +05:30
}
2020-06-05 22:11:42 +05:30
async function findDirectoryId ( path : string , branchId? : number ) : Promise < number | undefined > {
const { data : dirsResponse } = await crowdin . sourceFilesApi . listProjectDirectories ( PROJECT_ID , branchId ) ;
const dirs = dirsResponse . map ( ( dirData ) = > dirData . data ) ;
2019-03-09 05:40:06 +05:30
2020-06-05 22:11:42 +05:30
const result = path . 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 ) ;
2019-03-09 05:40:06 +05:30
2020-06-05 22:11:42 +05:30
return result || undefined ;
}
2019-03-09 05:40:06 +05:30
2020-06-05 22:11:42 +05:30
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 ;
2020-05-24 04:38:24 +05:30
}
2019-11-27 14:33:32 +05:30
2020-06-05 22:11:42 +05:30
// We're receiving files list without branch filter until https://github.com/crowdin/crowdin-api-client-js/issues/63
// will be resolved. But right now it doesn't matter because for each branch directories will have its own ids,
// so if the file is stored into the some directory, algorithm will find correct file.
const { data : filesResponse } = await crowdin . sourceFilesApi . listProjectFiles ( PROJECT_ID /*, branchId*/ ) ;
const files = filesResponse . map ( ( fileData ) = > fileData . data ) ;
2019-03-09 05:40:06 +05:30
2020-06-05 22:11:42 +05:30
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 branch = branchesList . find ( ( { data : branch } ) = > branch . name === branchName ) ;
return branch ? . 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 ) ;
2020-05-27 15:37:25 +05:30
2020-06-05 22:11:42 +05:30
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 ) ) ;
2019-03-09 05:40:06 +05:30
}
2020-05-27 15:37:25 +05:30
async function pull ( ) : Promise < void > {
2020-06-05 22:11:42 +05:30
const { branch : branchName } = getRepoInfo ( ) ;
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 ` ) ;
branchId = await findBranchId ( branchName ) ;
if ( ! branchId ) {
console . log ( ` Branch ${ chalk . green ( branchName ) } isn't found, will use ${ chalk . green ( 'master' ) } instead ` ) ;
}
}
2020-05-27 15:37:25 +05:30
console . log ( 'Loading file info...' ) ;
2020-06-05 22:11:42 +05:30
const fileId = await findFileId ( CROWDIN_FILE_PATH , branchId ) ;
if ( ! fileId ) {
throw new Error ( 'Cannot find the file' ) ;
}
2020-05-27 15:37:25 +05:30
2020-05-26 21:52:21 +05:30
console . log ( 'Pulling translation progress...' ) ;
2020-05-27 15:37:25 +05:30
const { data : translationProgress } = await crowdin . translationStatusApi . getFileProgress ( PROJECT_ID , fileId , 100 ) ;
2020-05-26 21:52:21 +05:30
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
2020-05-27 15:37:25 +05:30
const downloadingProgressBar = new ProgressBar ( 'Downloading translates :bar :percent | :cCurrent/:total' , {
2020-05-26 21:52:21 +05:30
total : localesToPull.length ,
incomplete : '\u2591' ,
complete : '\u2588' ,
width : Object.keys ( indexFileEntries ) . length - 1 ,
} ) ;
let downloadingReady = 0 ;
const promises = localesToPull . map ( async ( languageId ) : Promise < void > = > {
2020-05-27 15:37:25 +05:30
const { data : { url } } = await crowdin . translationsApi . buildProjectFileTranslation ( PROJECT_ID , fileId , {
2020-05-26 21:52:21 +05:30
targetLanguageId : languageId ,
exportApprovedOnly : true ,
} ) ;
2020-05-27 15:37:25 +05:30
const { data : fileContents } = await axios . get ( url , {
2020-05-26 21:52:21 +05:30
// Disable response parsing
transformResponse : [ ] ,
} ) ;
2020-05-27 15:37:25 +05:30
fs . writeFileSync ( getLocaleFilePath ( languageId ) , fileContents ) ;
2020-05-26 21:52:21 +05:30
2020-05-27 15:37:25 +05:30
downloadingProgressBar . update ( ++ downloadingReady / localesToPull . length , {
2020-05-26 21:52:21 +05:30
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' ) ) ;
}
2020-05-27 15:37:25 +05:30
async function push ( ) : Promise < void > {
2020-06-05 22:11:42 +05:30
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 > = [ {
2020-05-27 15:37:25 +05:30
name : 'disapproveTranslates' ,
type : 'confirm' ,
default : true ,
message : 'Disapprove changed lines?' ,
2020-06-05 22:11:42 +05:30
} ] ;
const { branch : branchName } = getRepoInfo ( ) ;
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 ( getRepoInfo ( ) . branch ) } ]? ` ,
} ) ;
}
2020-05-27 15:37:25 +05:30
2020-06-05 22:11:42 +05:30
const { disapproveTranslates , publishInBranch = false } = await prompt ( questions ) ;
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 ;
}
}
2020-05-27 15:37:25 +05:30
2020-06-05 22:11:42 +05:30
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 (
2020-05-27 15:37:25 +05:30
path . basename ( CROWDIN_FILE_PATH ) ,
fs . readFileSync ( getLocaleFilePath ( SOURCE_LANG ) ) ,
2020-05-24 04:38:24 +05:30
) ;
2020-05-20 22:05:52 +05:30
2020-06-05 22:11:42 +05:30
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 ) ;
}
2020-05-27 15:37:25 +05:30
console . log ( ch . green ( 'Success' ) ) ;
2019-03-09 05:40:06 +05:30
}
try {
2020-05-24 04:38:24 +05:30
const action = process . argv [ 2 ] ;
switch ( action ) {
case 'pull' :
2020-05-27 15:37:25 +05:30
pull ( ) ;
2020-05-24 04:38:24 +05:30
break ;
case 'push' :
push ( ) ;
break ;
default :
console . error ( ` Unknown action ${ action } ` ) ;
}
2019-03-09 05:40:06 +05:30
} catch ( exception ) {
2020-05-24 04:38:24 +05:30
console . error ( exception ) ;
2019-03-09 05:40:06 +05:30
}