diff --git a/src/languages.js b/src/languages.js index 2d23c55375..e934ab13c0 100644 --- a/src/languages.js +++ b/src/languages.js @@ -1,99 +1,73 @@ 'use strict'; -var fs = require('fs'); -var path = require('path'); -var async = require('async'); +const fs = require('fs'); +const path = require('path'); -var Languages = module.exports; -var languagesPath = path.join(__dirname, '../build/public/language'); +const util = require('util'); +const readFileAsync = util.promisify(fs.readFile); + +const Languages = module.exports; +const languagesPath = path.join(__dirname, '../build/public/language'); const files = fs.readdirSync(path.join(__dirname, '../public/vendor/jquery/timeago/locales')); Languages.timeagoCodes = files.filter(f => f.startsWith('jquery.timeago')).map(f => f.split('.')[2]); -Languages.get = function (language, namespace, callback) { - fs.readFile(path.join(languagesPath, language, namespace + '.json'), { encoding: 'utf-8' }, function (err, data) { - if (err) { - return callback(err); - } - - try { - data = JSON.parse(data) || {}; - } catch (e) { - return callback(e); - } - - callback(null, data); - }); +Languages.get = async function (language, namespace) { + let data = await readFileAsync(path.join(languagesPath, language, namespace + '.json'), 'utf8'); + try { + data = JSON.parse(data) || {}; + } catch (err) { + throw err; + } + return data; }; -var codeCache = null; -Languages.listCodes = function (callback) { +let codeCache = null; +Languages.listCodes = async function () { if (codeCache && codeCache.length) { - return callback(null, codeCache); + return codeCache; } - - fs.readFile(path.join(languagesPath, 'metadata.json'), 'utf8', function (err, file) { - if (err && err.code === 'ENOENT') { - return callback(null, []); + try { + const file = await readFileAsync(path.join(languagesPath, 'metadata.json'), 'utf8'); + const parsed = JSON.parse(file); + + codeCache = parsed.languages; + return parsed.languages; + } catch (err) { + if (err.code === 'ENOENT') { + return []; } - if (err) { - return callback(err); - } - - var parsed; - try { - parsed = JSON.parse(file); - } catch (e) { - return callback(e); - } - - var langs = parsed.languages; - codeCache = langs; - callback(null, langs); - }); + throw err; + } }; -var listCache = null; -Languages.list = function (callback) { +let listCache = null; +Languages.list = async function () { if (listCache && listCache.length) { - return callback(null, listCache); + return listCache; } - Languages.listCodes(function (err, codes) { - if (err) { - return callback(err); - } + const codes = await Languages.listCodes(); - async.map(codes, function (folder, next) { - var configPath = path.join(languagesPath, folder, 'language.json'); - - fs.readFile(configPath, 'utf8', function (err, file) { - if (err && err.code === 'ENOENT') { - return next(); - } - if (err) { - return next(err); - } - var lang; - try { - lang = JSON.parse(file); - } catch (e) { - return next(e); - } - next(null, lang); - }); - }, function (err, languages) { - if (err) { - return callback(err); + let languages = await Promise.all(codes.map(async function (folder) { + try { + const configPath = path.join(languagesPath, folder, 'language.json'); + const file = await readFileAsync(configPath, 'utf8'); + const lang = JSON.parse(file); + return lang; + } catch (err) { + if (err.code === 'ENOENT') { + return; } + throw err; + } + })); - // filter out invalid ones - languages = languages.filter(function (lang) { - return lang && lang.code && lang.name && lang.dir; - }); + // filter out invalid ones + languages = languages.filter(lang => lang && lang.code && lang.name && lang.dir); - listCache = languages; - callback(null, languages); - }); - }); + listCache = languages; + return languages; }; + +require('./promisify')(Languages); diff --git a/src/meta/languages.js b/src/meta/languages.js index fc907e60be..61770c0749 100644 --- a/src/meta/languages.js +++ b/src/meta/languages.js @@ -1,169 +1,136 @@ 'use strict'; -var path = require('path'); -var async = require('async'); -var fs = require('fs'); -var mkdirp = require('mkdirp'); -var rimraf = require('rimraf'); -var _ = require('lodash'); - -var file = require('../file'); -var Plugins = require('../plugins'); - -var buildLanguagesPath = path.join(__dirname, '../../build/public/language'); -var coreLanguagesPath = path.join(__dirname, '../../public/language'); - -function getTranslationMetadata(callback) { - async.waterfall([ - // generate list of languages and namespaces - function (next) { - file.walk(coreLanguagesPath, next); - }, - function (paths, next) { - var languages = []; - var namespaces = []; - - paths.forEach(function (p) { - if (!p.endsWith('.json')) { - return; - } - - var rel = path.relative(coreLanguagesPath, p).split(/[/\\]/); - var language = rel.shift().replace('_', '-').replace('@', '-x-'); - var namespace = rel.join('/').replace(/\.json$/, ''); - - if (!language || !namespace) { - return; - } - - languages.push(language); - namespaces.push(namespace); - }); - - next(null, { - languages: _.union(languages, Plugins.languageData.languages).sort().filter(Boolean), - namespaces: _.union(namespaces, Plugins.languageData.namespaces).sort().filter(Boolean), - }); - }, - - // save a list of languages to `${buildLanguagesPath}/metadata.json` - // avoids readdirs later on - function (ref, next) { - async.series([ - function (next) { - mkdirp(buildLanguagesPath, next); - }, - function (next) { - fs.writeFile(path.join(buildLanguagesPath, 'metadata.json'), JSON.stringify({ - languages: ref.languages, - namespaces: ref.namespaces, - }), next); - }, - ], function (err) { - next(err, ref); - }); - }, - ], callback); +const path = require('path'); +const fs = require('fs'); +const mkdirp = require('mkdirp'); +const rimraf = require('rimraf'); +const _ = require('lodash'); + +const util = require('util'); +const mkdirpAsync = util.promisify(mkdirp); +const rimrafAsync = util.promisify(rimraf); +const writeFileAsync = util.promisify(fs.writeFile); +const readFileAsync = util.promisify(fs.readFile); + +const file = require('../file'); +const Plugins = require('../plugins'); + +const buildLanguagesPath = path.join(__dirname, '../../build/public/language'); +const coreLanguagesPath = path.join(__dirname, '../../public/language'); + +async function getTranslationMetadata() { + const paths = await file.walk(coreLanguagesPath); + let languages = []; + let namespaces = []; + + paths.forEach(function (p) { + if (!p.endsWith('.json')) { + return; + } + + var rel = path.relative(coreLanguagesPath, p).split(/[/\\]/); + var language = rel.shift().replace('_', '-').replace('@', '-x-'); + var namespace = rel.join('/').replace(/\.json$/, ''); + + if (!language || !namespace) { + return; + } + + languages.push(language); + namespaces.push(namespace); + }); + + + languages = _.union(languages, Plugins.languageData.languages).sort().filter(Boolean); + namespaces = _.union(namespaces, Plugins.languageData.namespaces).sort().filter(Boolean); + + // save a list of languages to `${buildLanguagesPath}/metadata.json` + // avoids readdirs later on + await mkdirpAsync(buildLanguagesPath); + const result = { + languages: languages, + namespaces: namespaces, + }; + await writeFileAsync(path.join(buildLanguagesPath, 'metadata.json'), JSON.stringify(result)); + return result; } -function writeLanguageFile(language, namespace, translations, callback) { - var dev = global.env === 'development'; - var filePath = path.join(buildLanguagesPath, language, namespace + '.json'); +async function writeLanguageFile(language, namespace, translations) { + const dev = global.env === 'development'; + const filePath = path.join(buildLanguagesPath, language, namespace + '.json'); - async.series([ - async.apply(mkdirp, path.dirname(filePath)), - async.apply(fs.writeFile, filePath, JSON.stringify(translations, null, dev ? 2 : 0)), - ], callback); + await mkdirpAsync(path.dirname(filePath)); + await writeFileAsync(filePath, JSON.stringify(translations, null, dev ? 2 : 0)); } // for each language and namespace combination, // run through core and all plugins to generate // a full translation hash -function buildTranslations(ref, next) { - var namespaces = ref.namespaces; - var languages = ref.languages; - var plugins = _.values(Plugins.pluginsData).filter(function (plugin) { +async function buildTranslations(ref) { + const namespaces = ref.namespaces; + const languages = ref.languages; + const plugins = _.values(Plugins.pluginsData).filter(function (plugin) { return typeof plugin.languages === 'string'; }); - async.each(namespaces, function (namespace, next) { - async.each(languages, function (lang, next) { - var translations = {}; - - async.series([ - // core first - function (cb) { - fs.readFile(path.join(coreLanguagesPath, lang, namespace + '.json'), 'utf8', function (err, file) { - if (err) { - if (err.code === 'ENOENT') { - return cb(); - } - return cb(err); - } - - try { - Object.assign(translations, JSON.parse(file)); - cb(); - } catch (err) { - cb(err); - } - }); - }, - function (cb) { - // for each plugin, fallback in this order: - // 1. correct language string (en-GB) - // 2. old language string (en_GB) - // 3. corrected plugin defaultLang (en-US) - // 4. old plugin defaultLang (en_US) - async.each(plugins, function (pluginData, done) { - var pluginLanguages = path.join(__dirname, '../../node_modules/', pluginData.id, pluginData.languages); - var defaultLang = pluginData.defaultLang || 'en-GB'; - - async.eachSeries([ - defaultLang.replace('-', '_').replace('-x-', '@'), - defaultLang.replace('_', '-').replace('@', '-x-'), - lang.replace('-', '_').replace('-x-', '@'), - lang, - ], function (language, next) { - fs.readFile(path.join(pluginLanguages, language, namespace + '.json'), 'utf8', function (err, file) { - if (err) { - if (err.code === 'ENOENT') { - return next(null, false); - } - return next(err); - } - - try { - Object.assign(translations, JSON.parse(file)); - next(null, true); - } catch (err) { - next(err); - } - }); - }, done); - }, function (err) { - if (err) { - return cb(err); - } - - if (Object.keys(translations).length) { - writeLanguageFile(lang, namespace, translations, cb); - return; - } - cb(); - }); - }, - ], next); - }, next); - }, next); + const promises = []; + + namespaces.forEach(function (namespace) { + languages.forEach(function (language) { + promises.push(buildNamespaceLanguage(language, namespace, plugins)); + }); + }); + + await Promise.all(promises); +} + +async function buildNamespaceLanguage(lang, namespace, plugins) { + const translations = {}; + // core first + await assignFileToTranslations(translations, path.join(coreLanguagesPath, lang, namespace + '.json')); + + await Promise.all(plugins.map(pluginData => addPlugin(translations, pluginData, lang, namespace))); + + if (Object.keys(translations).length) { + await writeLanguageFile(lang, namespace, translations); + } +} + +async function addPlugin(translations, pluginData, lang, namespace) { + const pluginLanguages = path.join(__dirname, '../../node_modules/', pluginData.id, pluginData.languages); + const defaultLang = pluginData.defaultLang || 'en-GB'; + + // for each plugin, fallback in this order: + // 1. correct language string (en-GB) + // 2. old language string (en_GB) + // 3. corrected plugin defaultLang (en-US) + // 4. old plugin defaultLang (en_US) + const langs = [ + defaultLang.replace('-', '_').replace('-x-', '@'), + defaultLang.replace('_', '-').replace('@', '-x-'), + lang.replace('-', '_').replace('-x-', '@'), + lang, + ]; + + for (const language of langs) { + /* eslint-disable no-await-in-loop */ + await assignFileToTranslations(translations, path.join(pluginLanguages, language, namespace + '.json')); + } +} + +async function assignFileToTranslations(translations, path) { + try { + const fileData = await readFileAsync(path, 'utf8'); + Object.assign(translations, JSON.parse(fileData)); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + } } -exports.build = function buildLanguages(callback) { - async.waterfall([ - function (next) { - rimraf(buildLanguagesPath, next); - }, - getTranslationMetadata, - buildTranslations, - ], callback); +exports.build = async function buildLanguages() { + await rimrafAsync(buildLanguagesPath); + const data = await getTranslationMetadata(); + await buildTranslations(data); };