#!/usr/bin/env node /** * @fileoverview * 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 * @return {Promise} results array containing: * languages: array of supported languages * folders: array of tx resources corrsponding 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]); // eslint-disable-line no-undef }; /** * internal function to serialize saving category and folder name translations to avoid Freshdesk rate limit * @param {[type]} json [description] * @param {[type]} resource [description] * @param {[type]} locale [description] * @return {Promise} [description] */ const serializeNameSave = async (json, resource, locale) => { for (let [key, value] of Object.entries(json)) { // key is of the form _ 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 * @return {Promise} [description] */ const serializeFolderSave = async (json, locale) => { // json is a map of articles: // { // : { // title: {string: }, // description: {string: }, // tags: {string: : { // title: {string: }, // description: {string: }, // tags: {string: tag.length < 33); if (validTags.length !== tags.length) { process.stdout.write(`Warning: tags too long in ${id} for ${locale}\n`); } body.tags = validTags; } let status = await FD.updateArticleTranslation(id, freshdeskLocale(locale), body); if (status === -1) { 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 * @return {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 * @return {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 * @return {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(); // eslint-disable-line no-undef for (let i = 0; i < saveLanguages.length; i += BATCH_SIZE) { batchedPromises = batchedPromises .then(() => Promise.all( // eslint-disable-line 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 }); } };