You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
484 lines
18 KiB
484 lines
18 KiB
/* eslint-disable */
|
|
(function (global, factory) {
|
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('path'), require('is-valid-glob'), require('glob'), require('fs'), require('dot-object'), require('js-yaml')) :
|
|
typeof define === 'function' && define.amd ? define(['exports', 'path', 'is-valid-glob', 'glob', 'fs', 'dot-object', 'js-yaml'], factory) :
|
|
(global = global || self, factory(global.vueI18NExtract = {}, global.path, global.isValidGlob, global.glob, global.fs, global.dotObject, global.jsYaml));
|
|
}(this, (function (exports, path, isValidGlob, glob, fs, dot, yaml) {
|
|
path = path && Object.prototype.hasOwnProperty.call(path, 'default') ? path['default'] : path;
|
|
isValidGlob = isValidGlob && Object.prototype.hasOwnProperty.call(isValidGlob, 'default') ? isValidGlob['default'] : isValidGlob;
|
|
glob = glob && Object.prototype.hasOwnProperty.call(glob, 'default') ? glob['default'] : glob;
|
|
fs = fs && Object.prototype.hasOwnProperty.call(fs, 'default') ? fs['default'] : fs;
|
|
dot = dot && Object.prototype.hasOwnProperty.call(dot, 'default') ? dot['default'] : dot;
|
|
yaml = yaml && Object.prototype.hasOwnProperty.call(yaml, 'default') ? yaml['default'] : yaml;
|
|
|
|
function _extends() {
|
|
_extends = Object.assign || function (target) {
|
|
for (var i = 1; i < arguments.length; i++) {
|
|
var source = arguments[i];
|
|
|
|
for (var key in source) {
|
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
target[key] = source[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
return target;
|
|
};
|
|
|
|
return _extends.apply(this, arguments);
|
|
}
|
|
|
|
const decomment = require('decomment');
|
|
|
|
function readVueFiles(src) {
|
|
if (!isValidGlob(src)) {
|
|
throw new Error(`vueFiles isn't a valid glob pattern.`);
|
|
}
|
|
|
|
const targetFiles = glob.sync(src);
|
|
|
|
if (targetFiles.length === 0) {
|
|
throw new Error('vueFiles glob has no files.');
|
|
}
|
|
|
|
return targetFiles.map(f => {
|
|
const fileName = f.replace(process.cwd(), '');
|
|
return {
|
|
fileName,
|
|
path: f,
|
|
content: decomment.text(fs.readFileSync(f, 'utf8'))
|
|
};
|
|
});
|
|
}
|
|
|
|
function* getMatches(file, regExp, captureGroup = 1) {
|
|
while (true) {
|
|
const match = regExp.exec(file.content);
|
|
|
|
if (match === null) {
|
|
break;
|
|
}
|
|
|
|
const line = (file.content.substring(0, match.index).match(/\n/g) || []).length + 1;
|
|
yield {
|
|
path: match[captureGroup],
|
|
line,
|
|
file: file.fileName
|
|
};
|
|
}
|
|
}
|
|
/**
|
|
* Extracts translation keys from methods such as `$t` and `$tc`.
|
|
*
|
|
* - **regexp pattern**: (?:[$ .]tc?)\(
|
|
*
|
|
* **description**: Matches the sequence t( or tc(, optionally with either “$”, “.” or “ ” in front of it.
|
|
*
|
|
* - **regexp pattern**: (["'`])
|
|
*
|
|
* **description**: 1. capturing group. Matches either “"”, “'”, or “`”.
|
|
*
|
|
* - **regexp pattern**: ((?:[^\\]|\\.)*?)
|
|
*
|
|
* **description**: 2. capturing group. Matches anything except a backslash
|
|
* *or* matches any backslash followed by any character (e.g. “\"”, “\`”, “\t”, etc.)
|
|
*
|
|
* - **regexp pattern**: \1
|
|
*
|
|
* **description**: matches whatever was matched by capturing group 1 (e.g. the starting string character)
|
|
*
|
|
* @param file a file object
|
|
* @returns a list of translation keys found in `file`.
|
|
*/
|
|
|
|
|
|
function extractMethodMatches(file) {
|
|
const methodRegExp = /(?:[$ .]tc?)\(\s*?(["'`])((?:[^\\]|\\.)*?)\1/g;
|
|
return [...getMatches(file, methodRegExp, 2)];
|
|
}
|
|
|
|
function extractComponentMatches(file) {
|
|
const componentRegExp = /(?:<i18n)(?:.|\n)*?(?:[^:]path=("|'))(.*?)\1/gi;
|
|
return [...getMatches(file, componentRegExp, 2)];
|
|
}
|
|
|
|
function extractDirectiveMatches(file) {
|
|
const directiveRegExp = /v-t="'(.*?)'"/g;
|
|
return [...getMatches(file, directiveRegExp)];
|
|
}
|
|
|
|
function extractI18nItemsFromVueFiles(sourceFiles) {
|
|
return sourceFiles.reduce((accumulator, file) => {
|
|
const methodMatches = extractMethodMatches(file);
|
|
const componentMatches = extractComponentMatches(file);
|
|
const directiveMatches = extractDirectiveMatches(file);
|
|
return [...accumulator, ...methodMatches, ...componentMatches, ...directiveMatches];
|
|
}, []);
|
|
}
|
|
|
|
function parseVueFiles(vueFilesPath) {
|
|
const filesList = readVueFiles(vueFilesPath);
|
|
return extractI18nItemsFromVueFiles(filesList);
|
|
}
|
|
|
|
function readLangFiles(src) {
|
|
if (!isValidGlob(src)) {
|
|
throw new Error(`languageFiles isn't a valid glob pattern.`);
|
|
}
|
|
|
|
const targetFiles = glob.sync(src);
|
|
|
|
if (targetFiles.length === 0) {
|
|
throw new Error('languageFiles glob has no files.');
|
|
}
|
|
|
|
return targetFiles.map(f => {
|
|
const langPath = path.resolve(process.cwd(), f);
|
|
const extension = path.extname(langPath).toLowerCase();
|
|
const isJSON = extension === '.json';
|
|
const isYAML = extension === '.yaml' || extension === '.yml';
|
|
let langObj;
|
|
|
|
if (isJSON) {
|
|
langObj = JSON.parse(fs.readFileSync(langPath, 'utf8'));
|
|
} else if (isYAML) {
|
|
langObj = yaml.safeLoad(fs.readFileSync(langPath, 'utf8'));
|
|
} else {
|
|
langObj = eval(fs.readFileSync(langPath, 'utf8'));
|
|
}
|
|
|
|
const fileName = f.replace(process.cwd(), '');
|
|
return {
|
|
fileName,
|
|
path: langPath,
|
|
content: JSON.stringify(langObj)
|
|
};
|
|
});
|
|
}
|
|
|
|
function extractI18nItemsFromLanguageFiles(languageFiles, missingKeysOptions) {
|
|
return languageFiles.reduce((accumulator, file) => {
|
|
const language = file.fileName.substring(file.fileName.lastIndexOf('/') + 1, file.fileName.lastIndexOf('.'));
|
|
|
|
if (!accumulator[language]) {
|
|
accumulator[language] = [];
|
|
}
|
|
|
|
const fileContent = JSON.parse(file.content);
|
|
const flattenedObject = dot.dot(fileContent);
|
|
Object.keys(flattenedObject).forEach((key, index) => {
|
|
var _accumulator$language;
|
|
|
|
(_accumulator$language = accumulator[language]) == null ? void 0 : _accumulator$language.push({
|
|
line: index,
|
|
path: key,
|
|
file: file.fileName,
|
|
translated: ((missingKeysOptions.dotNotation ? dot.pick(key, fileContent) : fileContent[key]) || '').trim().length > 0
|
|
});
|
|
});
|
|
return accumulator;
|
|
}, {});
|
|
}
|
|
|
|
function sortedJSONStringify(obj, indent = 2) {
|
|
function flattenEntries([key, value]) {
|
|
return typeof value !== 'object' ? [[key, value]] : [[key, value], ...Object.entries(value).flatMap(flattenEntries)];
|
|
}
|
|
|
|
const allEntries = Object.entries(obj).flatMap(flattenEntries);
|
|
const sorted = allEntries.map(entry => entry[0]).sort();
|
|
return JSON.stringify(obj, sorted, indent);
|
|
}
|
|
|
|
function writeMissingToLanguage(resolvedLanguageFiles, missingKeys, missingKeysOptions, outputOptions) {
|
|
const languageFiles = readLangFiles(resolvedLanguageFiles);
|
|
languageFiles.forEach(languageFile => {
|
|
const languageFileContent = JSON.parse(languageFile.content);
|
|
missingKeys.forEach(item => {
|
|
if (item.language && languageFile.fileName.includes(item.language) || !item.language) {
|
|
const keyValue = item.language && missingKeysOptions.addValueForLanguages.includes(item.language) ? item.path : '';
|
|
|
|
if (missingKeysOptions.dotNotation) {
|
|
dot.str(item.path, keyValue, languageFileContent);
|
|
} else {
|
|
languageFileContent[item.path] = keyValue;
|
|
}
|
|
}
|
|
});
|
|
const fileExtension = path.extname(languageFile.fileName).toLowerCase();
|
|
const filePath = languageFile.path;
|
|
const stringifiedContent = outputOptions.sortKeys ? sortedJSONStringify(languageFileContent, outputOptions.indentationString) : JSON.stringify(languageFileContent, null, outputOptions.indentationString || 2);
|
|
|
|
if (fileExtension === '.json') {
|
|
fs.writeFileSync(filePath, stringifiedContent);
|
|
} else if (fileExtension === '.js') {
|
|
const jsFile = `export default ${stringifiedContent}; \n`;
|
|
fs.writeFileSync(filePath, jsFile);
|
|
} else if (fileExtension === '.yaml' || fileExtension === '.yml') {
|
|
const yamlFile = yaml.safeDump(languageFileContent);
|
|
fs.writeFileSync(filePath, yamlFile);
|
|
}
|
|
});
|
|
}
|
|
function parseLanguageFiles(languageFilesPath, missingKeysOptions) {
|
|
const filesList = readLangFiles(languageFilesPath);
|
|
return extractI18nItemsFromLanguageFiles(filesList, missingKeysOptions);
|
|
}
|
|
|
|
(function (VueI18NExtractReportTypes) {
|
|
VueI18NExtractReportTypes[VueI18NExtractReportTypes["None"] = 0] = "None";
|
|
VueI18NExtractReportTypes[VueI18NExtractReportTypes["Missing"] = 1] = "Missing";
|
|
VueI18NExtractReportTypes[VueI18NExtractReportTypes["Unused"] = 2] = "Unused";
|
|
VueI18NExtractReportTypes[VueI18NExtractReportTypes["Dynamic"] = 4] = "Dynamic";
|
|
VueI18NExtractReportTypes[VueI18NExtractReportTypes["All"] = 7] = "All";
|
|
})(exports.VueI18NExtractReportTypes || (exports.VueI18NExtractReportTypes = {}));
|
|
|
|
const mightBeUsedDynamically = function (languageItem, dynamicKeys) {
|
|
return dynamicKeys.some(dynamicKey => languageItem.path.includes(dynamicKey.path));
|
|
};
|
|
|
|
function extractI18NReport(parsedVueFiles, parsedLanguageFiles, reportType = exports.VueI18NExtractReportTypes.Missing + exports.VueI18NExtractReportTypes.Unused) {
|
|
const missingKeys = [];
|
|
const unusedKeys = [];
|
|
const dynamicKeys = [];
|
|
const dynamicReportEnabled = reportType & exports.VueI18NExtractReportTypes.Dynamic;
|
|
Object.keys(parsedLanguageFiles).forEach(language => {
|
|
let languageItems = parsedLanguageFiles[language];
|
|
parsedVueFiles.forEach(vueItem => {
|
|
const usedByVueItem = function (languageItem) {
|
|
return languageItem.path === vueItem.path || languageItem.path.startsWith(vueItem.path + '.');
|
|
};
|
|
|
|
if (dynamicReportEnabled && (vueItem.path.includes('${') || vueItem.path.endsWith('.'))) {
|
|
dynamicKeys.push(_extends({}, vueItem, {
|
|
language
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (!parsedLanguageFiles[language].some(usedByVueItem)) {
|
|
missingKeys.push(_extends({}, vueItem, {
|
|
language
|
|
}));
|
|
}
|
|
|
|
languageItems = languageItems.filter(languageItem => dynamicReportEnabled ? !mightBeUsedDynamically(languageItem, dynamicKeys) && !usedByVueItem(languageItem) : !usedByVueItem(languageItem));
|
|
});
|
|
unusedKeys.push(...languageItems.map(item => _extends({}, item, {
|
|
language
|
|
})));
|
|
});
|
|
let extracts = {};
|
|
|
|
if (reportType & exports.VueI18NExtractReportTypes.Missing) {
|
|
extracts = Object.assign(extracts, {
|
|
missingKeys
|
|
});
|
|
}
|
|
|
|
if (reportType & exports.VueI18NExtractReportTypes.Unused) {
|
|
extracts = Object.assign(extracts, {
|
|
unusedKeys
|
|
});
|
|
}
|
|
|
|
if (dynamicReportEnabled) {
|
|
extracts = Object.assign(extracts, {
|
|
dynamicKeys
|
|
});
|
|
}
|
|
|
|
return extracts;
|
|
}
|
|
function extractI18NLangFilesSynchronizationReport(parsedLanguageFiles, missingKeysOptions) {
|
|
const missingKeys = [];
|
|
|
|
if (missingKeysOptions.mainLanguageToSyncKeys) {
|
|
const otherLanguages = _extends({}, parsedLanguageFiles);
|
|
|
|
delete otherLanguages[missingKeysOptions.mainLanguageToSyncKeys];
|
|
const mainLanguageKeys = parsedLanguageFiles[missingKeysOptions.mainLanguageToSyncKeys] || [];
|
|
mainLanguageKeys.forEach(({
|
|
path
|
|
}) => {
|
|
Object.entries(otherLanguages).forEach(([lang, iItems]) => {
|
|
if (!iItems.some(iItem => iItem.path === path)) {
|
|
missingKeys.push({
|
|
path,
|
|
language: lang
|
|
});
|
|
}
|
|
});
|
|
});
|
|
} // detect empty translations for some languages (mainly for English)
|
|
|
|
|
|
Object.entries(parsedLanguageFiles).forEach(([lang, iItems]) => {
|
|
if (missingKeysOptions.addValueForLanguages.includes(lang)) {
|
|
iItems.filter(({
|
|
translated
|
|
}) => !translated).forEach(({
|
|
path
|
|
}) => {
|
|
missingKeys.push({
|
|
path,
|
|
language: lang
|
|
});
|
|
});
|
|
}
|
|
});
|
|
return {
|
|
missingKeys
|
|
};
|
|
}
|
|
async function writeReportToFile(report, writePath) {
|
|
const reportString = JSON.stringify(report);
|
|
return new Promise((resolve, reject) => {
|
|
fs.writeFile(writePath, reportString, err => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
if (!Array.prototype.flatMap) {
|
|
function flatMap(f, ctx) {
|
|
// @ts-ignore
|
|
return this.reduce((r, x, i, a) => r.concat(f.call(ctx, x, i, a)), []);
|
|
}
|
|
|
|
Array.prototype.flatMap = flatMap;
|
|
}
|
|
function createI18NReport(vueFiles, languageFiles, command, missingKeysOptions) {
|
|
const resolvedVueFiles = path.resolve(process.cwd(), vueFiles);
|
|
const resolvedLanguageFiles = path.resolve(process.cwd(), languageFiles);
|
|
const parsedVueFiles = parseVueFiles(resolvedVueFiles);
|
|
const parsedLanguageFiles = parseLanguageFiles(resolvedLanguageFiles, missingKeysOptions);
|
|
const reportType = command.dynamic ? exports.VueI18NExtractReportTypes.All : exports.VueI18NExtractReportTypes.Missing + exports.VueI18NExtractReportTypes.Unused;
|
|
const report = extractI18NReport(parsedVueFiles, parsedLanguageFiles, reportType);
|
|
const syncReport = extractI18NLangFilesSynchronizationReport(parsedLanguageFiles, missingKeysOptions); // @ts-ignore
|
|
|
|
report.missingKeys = [...report.missingKeys, ...syncReport.missingKeys];
|
|
return report;
|
|
}
|
|
|
|
function getStatisticsForKeys(keysData = []) {
|
|
const result = {
|
|
uniqueKeys: {},
|
|
keysNumbers: {}
|
|
};
|
|
result.uniqueKeys = keysData.reduce((accumulator, currentValue) => {
|
|
const langStatistics = accumulator[currentValue.language || '?'] = accumulator[currentValue.language || '?'] || {};
|
|
langStatistics[currentValue.path] = (langStatistics[currentValue.path] || 0) + 1;
|
|
return accumulator;
|
|
}, {
|
|
'en': {}
|
|
});
|
|
result.keysNumbers = Object.keys(result.uniqueKeys).reduce((acc, language) => {
|
|
acc[language] = Object.keys(result.uniqueKeys[language]).length;
|
|
return acc;
|
|
}, {});
|
|
return result;
|
|
}
|
|
|
|
async function reportCommand(command) {
|
|
const {
|
|
vueFiles,
|
|
languageFiles,
|
|
output,
|
|
add,
|
|
dynamic,
|
|
dotNotation,
|
|
addValueForLanguages,
|
|
mainLanguageToSyncKeys,
|
|
jsonSortKeys,
|
|
jsonIndentCharacters,
|
|
detailedReport
|
|
} = command;
|
|
const outputOptions = {
|
|
sortKeys: jsonSortKeys,
|
|
indentationString: ((indentationConfig = '2,space') => {
|
|
const charactersMap = {
|
|
'space': ' ',
|
|
'tab': '\t'
|
|
};
|
|
const [charactersAmount, character] = indentationConfig.split(',');
|
|
return ''.padEnd(Number(charactersAmount), charactersMap[character]);
|
|
})(jsonIndentCharacters)
|
|
};
|
|
const missingKeysOptions = {
|
|
dotNotation,
|
|
addValueForLanguages: addValueForLanguages instanceof Array ? addValueForLanguages : addValueForLanguages ? [addValueForLanguages] : [],
|
|
mainLanguageToSyncKeys
|
|
};
|
|
const report = createI18NReport(vueFiles, languageFiles, command, missingKeysOptions);
|
|
|
|
if (detailedReport) {
|
|
const normalizeKeysInfo = keyInfo => {
|
|
delete keyInfo.translated;
|
|
if (keyInfo.file) keyInfo.file = path.relative(process.cwd(), keyInfo.file).replace(/\\/g, '/');
|
|
return keyInfo;
|
|
};
|
|
|
|
if (report.missingKeys) console.info('missing keys: '), console.table([...report.missingKeys].map(normalizeKeysInfo));
|
|
if (report.unusedKeys) console.info('unused keys: '), console.table([...report.unusedKeys].map(normalizeKeysInfo));
|
|
if (report.dynamicKeys && dynamic && dynamic > 1) console.info('dynamic detected keys: '), console.table([...report.dynamicKeys].map(normalizeKeysInfo));
|
|
}
|
|
|
|
if (output) {
|
|
await writeReportToFile(report, path.resolve(process.cwd(), output));
|
|
console.log(`The report has been has been saved to ${output}`);
|
|
}
|
|
|
|
const summaryReport = _extends({
|
|
'Missing keys': getStatisticsForKeys(report.missingKeys).keysNumbers,
|
|
'Unused keys': getStatisticsForKeys(report.unusedKeys).keysNumbers
|
|
}, dynamic ? {
|
|
'Dynamic keys': getStatisticsForKeys(report.dynamicKeys).keysNumbers
|
|
} : {});
|
|
|
|
console.info('\nSummary report:');
|
|
console.table(summaryReport);
|
|
|
|
if (add && report.missingKeys && report.missingKeys.length > 0) {
|
|
const resolvedLanguageFiles = path.resolve(process.cwd(), languageFiles);
|
|
writeMissingToLanguage(resolvedLanguageFiles, report.missingKeys, missingKeysOptions, outputOptions);
|
|
console.log('The missing keys have been added to your languages files');
|
|
}
|
|
}
|
|
|
|
var report = {
|
|
__proto__: null,
|
|
createI18NReport: createI18NReport,
|
|
reportCommand: reportCommand,
|
|
readVueFiles: readVueFiles,
|
|
parseVueFiles: parseVueFiles,
|
|
writeMissingToLanguage: writeMissingToLanguage,
|
|
parseLanguageFiles: parseLanguageFiles,
|
|
get VueI18NExtractReportTypes () { return exports.VueI18NExtractReportTypes; },
|
|
extractI18NReport: extractI18NReport,
|
|
extractI18NLangFilesSynchronizationReport: extractI18NLangFilesSynchronizationReport,
|
|
writeReportToFile: writeReportToFile
|
|
};
|
|
|
|
var index = _extends({}, report);
|
|
|
|
exports.createI18NReport = createI18NReport;
|
|
exports.default = index;
|
|
exports.extractI18NLangFilesSynchronizationReport = extractI18NLangFilesSynchronizationReport;
|
|
exports.extractI18NReport = extractI18NReport;
|
|
exports.parseLanguageFiles = parseLanguageFiles;
|
|
exports.parseVueFiles = parseVueFiles;
|
|
exports.readVueFiles = readVueFiles;
|
|
exports.reportCommand = reportCommand;
|
|
exports.writeMissingToLanguage = writeMissingToLanguage;
|
|
exports.writeReportToFile = writeReportToFile;
|
|
|
|
})));
|
|
//# sourceMappingURL=vue-i18n-extract.umd.js.map
|