1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-23 02:10:12 +01:00
metamask-extension/development/verify-locale-strings.js
Mark Stacey 9f21317a30
Verify locales on CI (#7199)
* Add '--quiet' flag to verify locales script

The `--quiet` flag reduces the console output to just the essential
information for running in a CI environment. For each locale, it will
print the number of unused messages (if any).

* Add `verify-locales` script to lint CI job

The locales are now verified as part of the lint CI job. Any unused
messages detected will result in the job failing.
2019-09-21 13:31:33 -03:00

225 lines
6.6 KiB
JavaScript

// //////////////////////////////////////////////////////////////////////////////
//
// Locale verification script
//
// usage:
//
// node app/scripts/verify-locale-strings.js [<locale>] [--fix] [--quiet]
//
// This script will validate that locales have no unused messages. It will check
// the English locale against string literals found under `ui/`, and it will check
// other locales by comparing them to the English locale.
//
// A report will be printed to the console detailing any unused locales, and also
// any missing messages in the non-English locales.
//
// The if the optional '--fix' parameter is given, locales will be automatically
// updated to remove any unused messages.
//
// The optional '--quiet' parameter reduces the verbosity of the output, printing
// just a single summary of results for each locale verified
//
// //////////////////////////////////////////////////////////////////////////////
const fs = require('fs')
const path = require('path')
const { promisify } = require('util')
const log = require('loglevel')
const matchAll = require('string.prototype.matchall').getPolyfill()
const localeIndex = require('../app/_locales/index.json')
const readdir = promisify(fs.readdir)
const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.writeFile)
log.setDefaultLevel('info')
let fix = false
let specifiedLocale
for (const arg of process.argv.slice(2)) {
if (arg === '--fix') {
fix = true
} else if (arg === '--quiet') {
log.setLevel('error')
} else {
specifiedLocale = arg
}
}
main(specifiedLocale, fix)
.catch(error => {
log.error(error)
process.exit(1)
})
async function main (specifiedLocale, fix) {
if (specifiedLocale) {
log.info(`Verifying selected locale "${specifiedLocale}":\n`)
const locale = localeIndex.find(localeMeta => localeMeta.code === specifiedLocale)
const failed = locale.code === 'en' ?
await verifyEnglishLocale(fix) :
await verifyLocale(locale, fix)
if (failed) {
process.exit(1)
}
} else {
log.info('Verifying all locales:\n')
let failed = await verifyEnglishLocale(fix)
const localeCodes = localeIndex
.filter(localeMeta => localeMeta.code !== 'en')
.map(localeMeta => localeMeta.code)
for (const code of localeCodes) {
log.info() // Separate each locale report by a newline when not in '--quiet' mode
const localeFailed = await verifyLocale(code, fix)
failed = failed || localeFailed
}
if (failed) {
process.exit(1)
}
}
}
function getLocalePath (code) {
return path.resolve(__dirname, '..', 'app', '_locales', code, 'messages.json')
}
async function getLocale (code) {
try {
const localeFilePath = getLocalePath(code)
const fileContents = await readFile(localeFilePath, 'utf8')
return JSON.parse(fileContents)
} catch (e) {
if (e.code === 'ENOENT') {
log.error('Locale file not found')
} else {
log.error(`Error opening your locale ("${code}") file: `, e)
}
process.exit(1)
}
}
async function writeLocale (code, locale) {
try {
const localeFilePath = getLocalePath(code)
return writeFile(localeFilePath, JSON.stringify(locale, null, 2) + '\n', 'utf8')
} catch (e) {
if (e.code === 'ENOENT') {
log.error('Locale file not found')
} else {
log.error(`Error writing your locale ("${code}") file: `, e)
}
process.exit(1)
}
}
async function verifyLocale (code, fix = false) {
const englishLocale = await getLocale('en')
const targetLocale = await getLocale(code)
const extraItems = compareLocalesForMissingItems({ base: targetLocale, subject: englishLocale })
const missingItems = compareLocalesForMissingItems({ base: englishLocale, subject: targetLocale })
const englishEntryCount = Object.keys(englishLocale).length
const coveragePercent = 100 * (englishEntryCount - missingItems.length) / englishEntryCount
if (extraItems.length) {
console.log(`**${code}**: ${extraItems.length} unused messages`)
log.info('Extra items that should not be localized:')
extraItems.forEach(function (key) {
log.info(` - [ ] ${key}`)
})
} else {
log.info(`**${code}**: ${extraItems.length} unused messages`)
}
log.info(`${coveragePercent.toFixed(2)}% coverage`)
if (missingItems.length) {
log.info(`Missing items not present in localized file:`)
missingItems.forEach(function (key) {
log.info(` - [ ] ${key}`)
})
}
if (!extraItems.length && !missingItems.length) {
log.info('Full coverage : )')
}
if (extraItems.length > 0) {
if (fix) {
const newLocale = Object.assign({}, targetLocale)
for (const item of extraItems) {
delete newLocale[item]
}
await writeLocale(code, newLocale)
}
return true
}
}
async function verifyEnglishLocale (fix = false) {
const englishLocale = await getLocale('en')
const javascriptFiles = await findJavascriptFiles(path.resolve(__dirname, '..', 'ui'))
const regex = /'(\w+)'/g
const usedMessages = new Set()
for await (const fileContents of getFileContents(javascriptFiles)) {
for (const match of matchAll.call(fileContents, regex)) {
usedMessages.add(match[1])
}
}
// never consider these messages as unused
const messageExceptions = ['appName', 'appDescription']
const englishMessages = Object.keys(englishLocale)
const unusedMessages = englishMessages
.filter(message => !messageExceptions.includes(message) && !usedMessages.has(message))
if (unusedMessages.length) {
console.log(`**en**: ${unusedMessages.length} unused messages`)
log.info(`Messages not present in UI:`)
unusedMessages.forEach(function (key) {
log.info(` - [ ] ${key}`)
})
} else {
log.info('Full coverage : )')
return false
}
if (unusedMessages.length > 0 && fix) {
const newLocale = Object.assign({}, englishLocale)
for (const key of unusedMessages) {
delete newLocale[key]
}
await writeLocale('en', newLocale)
}
return true
}
async function findJavascriptFiles (rootDir) {
const javascriptFiles = []
const contents = await readdir(rootDir, { withFileTypes: true })
for (const file of contents) {
if (file.isDirectory()) {
javascriptFiles.push(...(await findJavascriptFiles(path.join(rootDir, file.name))))
} else if (file.isFile() && file.name.endsWith('.js')) {
javascriptFiles.push(path.join(rootDir, file.name))
}
}
return javascriptFiles
}
async function * getFileContents (filenames) {
for (const filename of filenames) {
yield readFile(filename, 'utf8')
}
}
function compareLocalesForMissingItems ({ base, subject }) {
return Object.keys(base).filter((key) => !subject[key])
}