const fs = require('fs')
const axios = require('axios')
const yaml = require('js-yaml')
const format = require('date-fns/format')
const _ = require('lodash')
const minimist = require('./minimist-ext.cjs')
const {
_: {
0: command,
1: taskIdOrKeyword
},
mentions = {}
} = minimist(process.argv.slice(2), { json: ['mentions'] })
// Separator used in GitHub Actions
const LOG_SEPARATOR = '---'
const getProjectConfig = () => {
try {
const configStrings = fs.readFileSync('crowdin-project-config.json')
return JSON.parse(configStrings)
} catch (error) {
console.error('Error parsing crowdin-project-config.json file. Please check crowdin-prject-config.json.example for the example.')
process.exit(1)
}
}
class Crowdin {
constructor ({ config }) {
const {
project_id: projectId,
api_token_env: apiTokenEnv,
directory_id: directoryId
} = config
const token = process.env[apiTokenEnv]
this.config = config
this.token = token
this.directoryId = directoryId
this.api = axios.create({
baseURL: `https://api.crowdin.com/api/v2/projects/${projectId}`,
timeout: 30000,
headers: { Authorization: `Bearer ${token}` }
})
}
async listProjectFiles (offset, limit = 500) {
const { data } = await this.api.get('files', {
params: {
directoryId: this.directoryId,
recursion: true,
limit,
offset
}
})
return data.data.map(({ data: item }) => ({
id: item.id,
path: item.path
}))
}
async listAllProjectFiles () {
const limit = 500
const maxOffset = 5000
let finalList = []
for (let offset = 0; offset < maxOffset; offset += limit) {
const data = await this.listProjectFiles(offset, limit)
if (_.isEmpty(data)) {
break
}
finalList = [
...finalList,
...data
]
}
return finalList
}
async createTask (title, languageId, fileIds) {
try {
const { data } = await this.api.post('tasks', {
title,
languageId,
fileIds,
type: 0,
skipAssignedStrings: true
})
return {
status: true,
data: data.data
}
} catch (e) {
return {
status: false,
message: _.get(e, 'response.data.error.message'),
code: _.get(e, 'response.data.error.code')
}
}
}
async closeTask (id) {
const { data } = await this.api.patch(`/tasks/${id}`, [{
op: 'replace',
path: '/status',
value: 'closed'
}])
return data
}
async listTasks (offset = 0, limit = 500, status = 'todo,in_progress,done') {
const { data } = await this.api.get('tasks', {
params: {
offset,
limit
}
})
console.log('Got tasks for', data.pagination, data.data.length)
return data.data
}
async listAllPendingTasks () {
const limit = 500
const maxOffset = 5000
let finalList = []
for (let offset = 0; offset < maxOffset; offset += limit) {
const data = await this.listTasks(offset, limit)
if (_.isEmpty(data)) {
break
}
finalList = [
...finalList,
...data
]
}
return finalList
}
}
const printTranslateLogs = (taskTitle, createdTasks) => {
if (_.isEmpty(createdTasks)) {
console.log(LOG_SEPARATOR)
return
}
console.log('Please help translate: ')
console.log(taskTitle)
console.log(LOG_SEPARATOR)
createdTasks.forEach((task) => {
const { id, targetLanguageId } = task
const url = `https://crowdin.com/project/holepunch/tasks/${id}`
const mention = mentions[targetLanguageId] ? `- <@${mentions[targetLanguageId]}>` : ''
console.log(`${url} - \`${targetLanguageId}\` ${mention}`)
})
}
const taskCreate = async (crowdin, projectConfig) => {
try {
const projectFiles = await crowdin.listAllProjectFiles()
const date = format(Date.now(), 'yyyy-MM-dd')
const taskTitle = `${projectConfig.name} updated on ${date} - ${Date.now()}`
const fileIds = projectFiles.map((file) => file.id)
console.log('creating task', taskTitle)
const createdTasks = []
for (const languageId of projectConfig.supported_langs) {
const taskCreatedResult = await crowdin.createTask(taskTitle, languageId, fileIds)
if (taskCreatedResult.status === true) {
console.log('Created task: ', languageId, taskCreatedResult.data.id)
createdTasks.push(taskCreatedResult.data)
} else {
console.log('No task created: ', taskCreatedResult.message)
}
}
printTranslateLogs(taskTitle, createdTasks)
} catch (error) {
console.log(error)
console.log('Response error', _.get(error, 'response.data'))
}
}
const taskCloseById = async (crowdin) => {
const id = taskIdOrKeyword
if (!id) {
console.error('No ID found. Example command: ./crowdin.js task-close-by-id 123')
process.exit(1)
}
try {
const res = await crowdin.closeTask(id)
console.log(res)
} catch (error) {
console.log(error)
console.log('Response error', JSON.stringify(_.get(error, 'response.data')))
}
}
const taskClose = async (crowdin) => {
const idToFind = taskIdOrKeyword
if (!idToFind) {
console.error('No ID found. Example command: ./crowdin.js task-close 123')
process.exit(1)
}
try {
const res = await crowdin.listAllPendingTasks()
console.log('Total pending tasks: ', res.length)
const taskToClose = res.filter((task) => task.data.title.includes(idToFind))
for (const task of taskToClose) {
console.log('closing task', task.data.id)
await crowdin.closeTask(task.data.id)
}
} catch (error) {
console.log(error)
console.log('Response error', JSON.stringify(_.get(error, 'response.data')))
}
}
const searchTasks = async (crowdin) => {
const keyword = taskIdOrKeyword
if (!keyword) {
console.error('No keyword found. Example command: ./crowdin.js task-search bfx-ui-pay')
process.exit(1)
}
try {
const res = await crowdin.listAllPendingTasks()
console.log('Total pending tasks: ', res.length)
const taskToShow = res.filter((task) => task.data.title.includes(keyword))
for (const task of taskToShow) {
console.log(task.data.id)
console.log(task.data.title)
console.log(task.data.translationUrl)
console.log('------')
}
} catch (error) {
console.log(error)
console.log('Response error', JSON.stringify(_.get(error, 'response.data')))
}
}
const run = async () => {
const crowdinConfig = yaml.load(fs.readFileSync('crowdin.yml', 'utf8'))
const projectConfig = getProjectConfig()
crowdinConfig.directory_id = projectConfig.directory_id
const crowdin = new Crowdin({ config: crowdinConfig })
switch (command) {
case 'task-create':
await taskCreate(crowdin, projectConfig)
break
case 'task-close-by-id':
await taskCloseById(crowdin)
break
case 'task-close':
await taskClose(crowdin)
break
case 'task-search':
await searchTasks(crowdin)
break
default:
console.log('Available commands: ')
console.log('task-create: Create tasks and log on console')
console.log('task-close-by-id: Close a task by its ID')
console.log('task-close: Close a task by our own ID from task-create command')
console.log('task-search: Search pending tasks by keyword')
break
}
}
run()