/* eslint-env node */ /* eslint-disable no-console */ import fs from 'fs'; import { sync as globSync } from 'glob'; import { sync as mkdirpSync } from 'mkdirp'; import chalk from 'chalk'; import prompt from 'prompt'; import localesMap from 'app/i18n'; const MESSAGES_PATTERN = `${__dirname}/../../build/messages/**/*.json`; const LANG_DIR = `${__dirname}/../app/i18n`; const DEFAULT_LOCALE = 'en'; const SUPPORTED_LANGS = [DEFAULT_LOCALE, ...Object.keys(localesMap)]; /** * Aggregates the default messages that were extracted from the app's * React components via the React Intl Babel plugin. An error will be thrown if * there are messages in different components that use the same `id`. The result * is a flat collection of `id: message` pairs for the app's default locale. */ let idToFileMap: { [key: string]: string[] } = {}; let duplicateIds: string[] = []; const collectedMessages = globSync(MESSAGES_PATTERN) .map(filename => [filename, JSON.parse(fs.readFileSync(filename, 'utf8'))]) .reduce((collection, [file, descriptors]) => { descriptors.forEach(({ id, defaultMessage }) => { if (collection.hasOwnProperty(id)) { duplicateIds.push(id); } collection[id] = defaultMessage; idToFileMap[id] = (idToFileMap[id] || []).concat(file); }); return collection; }, {}); if (duplicateIds.length) { console.log('\nFound duplicated ids:'); duplicateIds.forEach(id => console.log(`${chalk.yellow(id)}:\n - ${idToFileMap[id].join('\n - ')}\n`), ); console.log(chalk.red('Please correct the errors above to proceed further!')); process.exit(1); } duplicateIds = []; idToFileMap = {}; /** * Making a diff with the previous DEFAULT_LOCALE version */ const defaultMessagesPath = `${LANG_DIR}/${DEFAULT_LOCALE}.json`; let keysToUpdate: string[] = []; let keysToAdd: string[] = []; let keysToRemove: string[] = []; const keysToRename: Array<[string, string]> = []; const isNotMarked = (value: string) => value.slice(0, 2) !== '--'; const prevMessages: { [key: string]: string } = readJSON(defaultMessagesPath); const prevMessagesMap = Object.entries(prevMessages).reduce( (acc, [key, value]) => { if (acc[value]) { acc[value].push(key); } else { acc[value] = [key]; } return acc; }, {} as { [key: string]: string[] }, ); keysToAdd = Object.keys(collectedMessages).filter(key => !prevMessages[key]); keysToRemove = Object.keys(prevMessages) .filter(key => !collectedMessages[key]) .filter(isNotMarked); keysToUpdate = Object.entries(prevMessages).reduce( (acc, [key, message]) => acc.concat( collectedMessages[key] && collectedMessages[key] !== message ? key : [], ), [] as string[], ); // detect keys to rename, mutating keysToAdd and keysToRemove [...keysToAdd].forEach(toKey => { const keys = prevMessagesMap[collectedMessages[toKey]] || []; const fromKey = keys.find(key => keysToRemove.indexOf(key) > -1); if (fromKey) { keysToRename.push([fromKey, toKey]); keysToRemove.splice(keysToRemove.indexOf(fromKey), 1); keysToAdd.splice(keysToAdd.indexOf(toKey), 1); } }); if ( !keysToAdd.length && !keysToRemove.length && !keysToUpdate.length && !keysToRename.length ) { console.log(chalk.green('Everything is up to date!')); process.exit(); } console.log( chalk.magenta(`The diff relative to default locale (${DEFAULT_LOCALE}) is:`), ); if (keysToRemove.length) { console.log('The following keys will be removed:'); console.log( [chalk.red('\n - '), keysToRemove.join(chalk.red('\n - ')), '\n'].join(''), ); } if (keysToAdd.length) { console.log('The following keys will be added:'); console.log( [chalk.green('\n + '), keysToAdd.join(chalk.green('\n + ')), '\n'].join(''), ); } if (keysToUpdate.length) { console.log('The following keys will be updated:'); console.log( [ chalk.yellow('\n @ '), keysToUpdate.join(chalk.yellow('\n @ ')), '\n', ].join(''), ); } if (keysToRename.length) { console.log('The following keys will be renamed:\n'); console.log( keysToRename.reduce( (str, pair) => [str, pair[0], chalk.yellow(' -> '), pair[1], '\n'].join(''), '', ), ); } prompt.start(); prompt.get( { properties: { apply: { description: 'Apply changes? [Y/n]', pattern: /^y|n$/i, message: 'Please enter "y" or "n"', default: 'y', before: value => value.toLowerCase() === 'y', }, }, }, (err, resp) => { console.log('\n'); if (err || !resp.apply) { return console.log(chalk.red('Aborted')); } buildLocales(); console.log(chalk.green('All locales was successfuly built')); }, ); function buildLocales() { mkdirpSync(LANG_DIR); SUPPORTED_LANGS.map(lang => { const destPath = `${LANG_DIR}/${lang}.json`; const newMessages = readJSON(destPath); keysToRename.forEach(([fromKey, toKey]) => { newMessages[toKey] = newMessages[fromKey]; delete newMessages[fromKey]; }); keysToRemove.forEach(key => { delete newMessages[key]; }); keysToUpdate.forEach(key => { newMessages[`--${key}`] = newMessages[key]; newMessages[key] = collectedMessages[key]; }); keysToAdd.forEach(key => { newMessages[key] = collectedMessages[key]; }); const sortedKeys = Object.keys(newMessages).sort((key1, key2) => { key1 = key1.replace(/^-+/, ''); key2 = key2.replace(/^-+/, ''); return key1 < key2 || !isNotMarked(key1) ? -1 : 1; }); const sortedNewMessages = sortedKeys.reduce((acc, key) => { acc[key] = newMessages[key]; return acc; }, {}); fs.writeFileSync( destPath, `${JSON.stringify(sortedNewMessages, null, 4)}\n`, ); }); } function readJSON(destPath) { try { return JSON.parse(fs.readFileSync(destPath, 'utf8')); } catch (err) { console.log( chalk.yellow(`Can not read ${destPath}. The new file will be created.`), `(${err.message})`, ); } return {}; }