#!/usr/bin/env node /** * @file * Helper functions for syncing Freshdesk knowledge base articles with Transifex */ const FreshdeskApi = require('./freshdesk-api.js') const fs = require('fs') const fsPromises = fs.promises const mkdirp = require('mkdirp') const { txPull, txResourcesObjects, txAvailableLanguages } = require('../lib/transifex.js') const FD = new FreshdeskApi('https://mitscratch.freshdesk.com', process.env.FRESHDESK_TOKEN) const TX_PROJECT = 'scratch-help' const freshdeskLocale = locale => { // map between Transifex locale and Freshdesk. Two letter codes are usually fine const localeMap = { es_419: 'es-LA', ja: 'ja-JP', 'ja-Hira': 'ja-JP', lv: 'lv-LV', nb: 'nb-NO', nn: 'nb-NO', pt: 'pt-PT', pt_BR: 'pt-BR', ru: 'ru-RU', sv: 'sv-SE', zh_CN: 'zh-CN', zh_TW: 'zh-TW', } return localeMap[locale] || locale } /** * Pull metadata from Transifex for the scratch-help project * @returns {Promise} results array containing: * languages: array of supported languages * folders: array of tx resources corresponding to Freshdesk folders * names: array of tx resources corresponding to the Freshdesk metadata */ exports.getInputs = async () => { const resources = await txResourcesObjects(TX_PROJECT) const languages = await txAvailableLanguages(TX_PROJECT) // there are three types of resources differentiated by the file type const folders = resources.filter(resource => resource.i18n_type === 'STRUCTURED_JSON') const names = resources.filter(resource => resource.i18n_type === 'KEYVALUEJSON') // ignore the yaml type because it's not possible to update via API return Promise.all([languages, folders, names]) } /* * internal function to serialize saving category and folder name translations to avoid Freshdesk rate limit */ const serializeNameSave = async (json, resource, locale) => { for (const [key, value] of Object.entries(json)) { // key is of the form <name>_<id> const words = key.split('_') const id = words[words.length - 1] let status = 0 if (resource.name === 'categoryNames_json') { status = await FD.updateCategoryTranslation(id, freshdeskLocale(locale), { name: value }) } if (resource.name === 'folderNames_json') { status = await FD.updateFolderTranslation(id, freshdeskLocale(locale), { name: value }) } if (status === -1) { process.exitCode = 1 } } } /** * Internal function serialize Freshdesk requests to avoid getting rate limited * @param {object} json object with keys corresponding to article ids * @param {string} locale language code * @returns {Promise} [description] */ const serializeFolderSave = async (json, locale) => { // json is a map of articles: // { // <id>: { // title: {string: <title-value>}, // description: {string: <description-value>}, // tags: {string: <comma separated strings} // optional // }, // <id>: { // title: {string: <title-value>}, // description: {string: <description-value>}, // tags: {string: <comma separated strings} // optional // } // } for (const [id, value] of Object.entries(json)) { const body = { title: value.title.string, description: value.description.string, status: 2, // set status to published } if (Object.prototype.hasOwnProperty.call(value, 'tags')) { const tags = value.tags.string.split(',') const validTags = tags.filter(tag => tag.length < 33) if (validTags.length !== tags.length) { process.stdout.write(`Warning: tags too long in ${id} for ${locale}\n`) } body.tags = validTags } const status = await FD.updateArticleTranslation(id, freshdeskLocale(locale), body) if (status === -1) { // eslint-disable-next-line require-atomic-updates -- I promise that `process` won't change across `await` process.exitCode = 1 } } return 0 } /** * Process Transifex resource corresponding to a Knowledge base folder on Freshdesk * @param {object} folder Transifex resource json corresponding to a KB folder * @param {string} locale locale to pull and submit to Freshdesk * @returns {Promise} [description] */ exports.localizeFolder = async (folder, locale) => { txPull(TX_PROJECT, folder.slug, locale, { mode: 'default' }) .then(data => { serializeFolderSave(data, locale) }) .catch(e => { process.stdout.write(`Error processing ${folder.slug}, ${locale}: ${e.message}\n`) process.exitCode = 1 // not ok }) } /** * Save Transifex resource corresponding to a Knowledge base folder locally for debugging * @param {object} folder Transifex resource json corresponding to a KB folder * @param {string} locale locale to pull and save * @returns {Promise} [description] */ exports.debugFolder = async (folder, locale) => { mkdirp.sync('tmpDebug') txPull(TX_PROJECT, folder.slug, locale, { mode: 'default' }) .then(data => { fsPromises.writeFile(`tmpDebug/${folder.slug}_${locale}.json`, JSON.stringify(data, null, 2)) }) .catch(e => { process.stdout.write(`Error processing ${folder.slug}, ${locale}: ${e.message}\n`) process.exitCode = 1 // not ok }) } /** * Process KEYVALUEJSON resources from scratch-help on transifex * Category and Folder names are stored as plain json * @param {object} resource Transifex resource json for either CategoryNames or FolderNames * @param {string} locale locale to pull and submit to Freshdesk * @returns {Promise} [description] */ exports.localizeNames = async (resource, locale) => { txPull(TX_PROJECT, resource.slug, locale, { mode: 'default' }) .then(data => { serializeNameSave(data, resource, locale) }) .catch(e => { process.stdout.write(`Error saving ${resource.slug}, ${locale}: ${e.message}\n`) process.exitCode = 1 // not ok }) } const BATCH_SIZE = 2 /* * save resource items in batches to reduce rate limiting errors * @param {object} item Transifex resource json, used for 'slug' * @param {array} languages Array of languages to save * @param {function} saveFn Async function to use to save the item * @return {Promise} */ exports.saveItem = async (item, languages, saveFn) => { const saveLanguages = languages.filter(l => l !== 'en') // exclude English from update let batchedPromises = Promise.resolve() for (let i = 0; i < saveLanguages.length; i += BATCH_SIZE) { batchedPromises = batchedPromises .then(() => Promise.all(saveLanguages.slice(i, i + BATCH_SIZE).map(l => saveFn(item, l)))) .catch(err => { process.stdout.write(`Error saving item:${err.message}\n${JSON.stringify(item, null, 2)}\n`) process.exitCode = 1 // not ok }) } }