|
const _ = require("lodash"); |
|
const fs = require("fs"); |
|
const path = require("path"); |
|
const chalk = require("chalk"); |
|
const { parse, traverse } = require("@babel/core"); |
|
const t = require("@babel/types"); |
|
const glob = require("glob"); |
|
const root = path.resolve(__dirname + "/../"); |
|
const translations_dir = `${root}/src/translations`; |
|
|
|
let translations; |
|
let seen = {}; |
|
let warn = {}; |
|
|
|
console.log(chalk.bold`Loading current translations...`); |
|
glob(`${translations_dir}/+([a-z0-9]).json`, (err, matches) => { |
|
translations = _.fromPairs( |
|
matches.map(filename => { |
|
return [ |
|
filename.match(/([a-z0-9]+)\.json/)[1], |
|
JSON.parse(fs.readFileSync(filename, "utf8")) |
|
]; |
|
}) |
|
); |
|
console.log(translations); |
|
|
|
console.log(""); |
|
console.log(chalk.bold`Scanning source files...`); |
|
glob( |
|
`${root}/src/**/*/!(*.spec|*.test|*.stories|*.dev).{js,jsx,ts,tsx}`, |
|
(err, matches) => { |
|
matches.forEach(scanFile); |
|
|
|
console.log(""); |
|
console.log(chalk.bold`Writing back new translations:`); |
|
console.log(translations); |
|
Object.entries(translations).forEach(([lang, data]) => { |
|
fs.writeFileSync( |
|
`${translations_dir}/${lang}.json`, |
|
JSON.stringify(data, null, 2), |
|
"utf8" |
|
); |
|
}); |
|
|
|
const TODO = {}; |
|
|
|
console.log(""); |
|
console.log(chalk.bold`Yet to be translated dummy translation content:`); |
|
const dummy = (TODO[ |
|
"Yet to be translated dummy translation content" |
|
] = {}); |
|
Object.entries(translations).forEach(([lang, data]) => { |
|
dummy[lang] = {}; |
|
Object.keys(data).forEach(str => { |
|
if (str === data[str]) { |
|
dummy[lang][str] = true; |
|
console.log(" -", chalk.bold.yellow(str)); |
|
} |
|
}); |
|
}); |
|
|
|
console.log(""); |
|
console.log(chalk.bold`Possibly orphaned translations:`); |
|
const orphaned = (TODO["Possibly orphaned translations"] = {}); |
|
Object.entries(translations).forEach(([lang, data]) => { |
|
orphaned[lang] = {}; |
|
Object.keys(data).forEach(str => { |
|
if (!seen[str]) { |
|
orphaned[lang][str] = true; |
|
console.log(" -", chalk.bold.magenta(str)); |
|
} |
|
}); |
|
}); |
|
|
|
console.log(""); |
|
console.log(chalk.bold`Untranslated content in source code:`); |
|
TODO["Untranslated content in source code"] = _.mapValues( |
|
warn, |
|
o => true |
|
); |
|
Object.entries(warn).forEach(([text, [filename, startLoc]]) => { |
|
console.log(" -", chalk.bold.cyan(text), "@", filename, { |
|
...startLoc |
|
}); |
|
}); |
|
|
|
fs.writeFileSync( |
|
`${translations_dir}/_todo.json`, |
|
JSON.stringify(TODO, null, 2), |
|
"utf8" |
|
); |
|
|
|
console.log(""); |
|
} |
|
); |
|
}); |
|
|
|
const colorings = { |
|
t: chalk.bold.green, |
|
Trans: chalk.bold.blue, |
|
i18nKey: chalk.bold.white |
|
}; |
|
|
|
function foundStr(type, str) { |
|
if (!seen[str]) { |
|
seen[str] = true; |
|
console.log(" - found", type, colorings[type](str)); |
|
Object.keys(translations).forEach(lang => { |
|
if (typeof translations[lang][str] === "undefined") { |
|
translations[lang][str] = str; // default to default language |
|
console.log(" - added to", chalk.inverse(` ${lang} `)); |
|
} |
|
}); |
|
} |
|
} |
|
|
|
function scanFile(filename) { |
|
const f = filename.replace(root + "/", ""); |
|
console.log(chalk.dim(f)); |
|
const code = fs.readFileSync(filename, "utf8"); |
|
const ast = parse(code, { |
|
presets: [ |
|
["@babel/preset-typescript", { isTSX: true, allExtensions: true }] |
|
], |
|
plugins: [ |
|
["@babel/plugin-proposal-decorators", { legacy: true }], |
|
"@babel/plugin-syntax-dynamic-import" |
|
], |
|
filename |
|
}); |
|
traverse(ast, { |
|
CallExpression(path) { |
|
if (t.isIdentifier(path.node.callee) && path.node.callee.name === "t") { |
|
if ( |
|
(path.node.arguments.length === 1 || |
|
path.node.arguments.length === 2) && |
|
t.isStringLiteral(path.node.arguments[0]) |
|
) { |
|
const str = path.node.arguments[0].value; |
|
foundStr("t", str); |
|
} else { |
|
console.log( |
|
" -", |
|
chalk.red`t not called with 1 or 2 string literals`, |
|
"@", |
|
f, |
|
{ |
|
...path.node.loc.start |
|
} |
|
); |
|
return; |
|
} |
|
} |
|
}, |
|
JSXText(path) { |
|
const text = path.node.value.trim(); |
|
if (text && !warn[text] && text.match(/[a-zA-Z]/) && text.length > 1) { |
|
console.log(" - not translated?", chalk.bold.red(text)); |
|
warn[text] = [f, path.node.loc.start]; |
|
} |
|
}, |
|
JSXElement(path) { |
|
if ( |
|
t.isJSXIdentifier(path.node.openingElement.name) && |
|
path.node.openingElement.name.name === "Trans" |
|
) { |
|
const i18nKey = path.node.openingElement.attributes.find( |
|
attr => attr.name.name === "i18nKey" |
|
); |
|
if (i18nKey) { |
|
foundStr("i18nKey", i18nKey.value.value); |
|
} else { |
|
foundStr("Trans", nodesToString(path.node.children)); |
|
} |
|
path.skip(); |
|
} |
|
} |
|
}); |
|
} |
|
|
|
// See [https://github.com/i18next/i18next-scanner/blob/master/src/nodes-to-string.js] |
|
function nodesToString(nodes) { |
|
let memo = ""; |
|
let nodeIndex = 0; |
|
nodes.forEach((node, i) => { |
|
if (t.isJSXText(node) || t.isStringLiteral(node)) { |
|
const value = node.value |
|
.replace(/^[\r\n]+\s*/g, "") // remove leading spaces containing a leading newline character |
|
.replace(/[\r\n]+\s*$/g, "") // remove trailing spaces containing a leading newline character |
|
.replace(/[\r\n]+\s*/g, " "); // replace spaces containing a leading newline character with a single space character |
|
|
|
if (!value) { |
|
return null; |
|
} |
|
memo += value; |
|
} else if (t.isJSXExpressionContainer(node)) { |
|
const { expression = {} } = node; |
|
|
|
if (t.isNumericLiteral(expression)) { |
|
// Numeric literal is ignored in react-i18next |
|
memo += ""; |
|
} |
|
if (t.isStringLiteral(expression)) { |
|
memo += expression.value; |
|
} else if ( |
|
t.isObjectExpression(expression) && |
|
_.get(expression, "properties[0].type") === "ObjectProperty" |
|
) { |
|
// memo += `<${nodeIndex}>{{${ |
|
// expression.properties[0].key.name |
|
// }}}</${nodeIndex}>`; |
|
memo += `{{${expression.properties[0].key.name}}}`; |
|
} else { |
|
console.error( |
|
`Unsupported JSX expression. Only static values or {{interpolation}} blocks are supported. Got ${expression.type}:` |
|
); |
|
return null; |
|
} |
|
} else if (node.children) { |
|
memo += `<${nodeIndex}>${nodesToString(node.children)}</${nodeIndex}>`; |
|
} |
|
|
|
++nodeIndex; |
|
}); |
|
|
|
return memo; |
|
} |