Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,16 @@ You can generate a default configuration file using `npx vue-i18n-extract init`
* CLI argument: `--missing-translation-string`, `--missingTranslationString`
* Required: No
* Default: `''`
* Type: `string` or `null`
* Type: `string` or `null` or a placeholder variable `{{t}}`
* Description: Text to use when missing translations are added to the translation files.
* Examples:
* `'Translation missing'`: Use "Translation missing" as default key.
* `null`: Add the translation key to the file, but don't add a default translation. This will trigger `vue-i18n`'s the missingHandler.
- Examples:
- `'Translation missing'`: Use "Translation missing" as default key.
- `null`: Add the translation key to the file, but don't add a default translation. This will trigger `vue-i18n`'s the missingHandler.
- `{{t}}`: This is a placeholder variable used for translation keys in your project. When processing templates or content, this placeholder will be replaced with the appropriate translation key. You can use this variable in different contexts such as:
- Inside double brackets: `[[{{t}}]]`
- As part of todo comments: `TODO: {{t}}`
- In any other text where a translation key needs to be inserted
- For example, if your key is "user.greeting", using `'Missing: {{t}}'` would generate "Missing: user.greeting" as the default text.

## Supported `vue-i18n` Formats

Expand Down
12 changes: 6 additions & 6 deletions dist/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export declare type ReportOptions = {
export type ReportOptions = {
vueFiles: string;
languageFiles: string;
output?: string;
Expand All @@ -16,25 +16,25 @@ export declare enum DetectionType {
Unused = "unused",
Dynamic = "dynamic"
}
export declare type SimpleFile = {
export type SimpleFile = {
fileName: string;
path: string;
content: string;
};
export declare type I18NItem = {
export type I18NItem = {
line?: number;
path: string;
file?: string;
language?: string;
};
export declare type I18NItemWithBounding = I18NItem & {
export type I18NItemWithBounding = I18NItem & {
previousCharacter: string;
nextCharacter: string;
};
export declare type I18NLanguage = {
export type I18NLanguage = {
[language: string]: I18NItem[];
};
export declare type I18NReport = {
export type I18NReport = {
missingKeys: I18NItem[];
unusedKeys: I18NItem[];
maybeDynamicKeys: I18NItem[];
Expand Down
90 changes: 25 additions & 65 deletions dist/vue-i18n-extract.modern.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,13 @@ import Dot from 'dot-object';
import yaml from 'js-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 _extends = Object.assign ? Object.assign.bind() : function (n) {
for (var e = 1; e < arguments.length; e++) {
var t = arguments[e];
for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);
}

return target;
};

return _extends.apply(this, arguments);
return n;
}, _extends.apply(null, arguments);
}

var defaultConfig = {
Expand All @@ -46,24 +38,20 @@ function resolveConfig() {
run: false
}).options;
let options;

try {
const pathToConfigFile = path.resolve(process.cwd(), './vue-i18n-extract.config.js'); // eslint-disable-next-line @typescript-eslint/no-var-requires

const pathToConfigFile = path.resolve(process.cwd(), './vue-i18n-extract.config.js');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const configOptions = require(pathToConfigFile);

console.info(`\nUsing config file found at ${pathToConfigFile}`);
options = _extends({}, configOptions, argvOptions);
} catch (_unused) {
options = argvOptions;
}

options.exclude = Array.isArray(options.exclude) ? options.exclude : [options.exclude];
return options;
}

var DetectionType;

(function (DetectionType) {
DetectionType["Missing"] = "missing";
DetectionType["Unused"] = "unused";
Expand All @@ -74,17 +62,13 @@ function readVueFiles(src) {
// Replace backslash path segments to make the path work with the glob package.
// https://github.com/Spittal/vue-i18n-extract/issues/159
const normalizedSrc = src.replace(/\\/g, '/');

if (!isValidGlob(normalizedSrc)) {
throw new Error(`vueFiles isn't a valid glob pattern.`);
}

const targetFiles = glob.sync(normalizedSrc);

if (targetFiles.length === 0) {
throw new Error('vueFiles glob has no files.');
}

return targetFiles.map(f => {
const fileName = f.replace(process.cwd(), '.');
return {
Expand All @@ -94,15 +78,12 @@ function readVueFiles(src) {
};
});
}

function* getMatches(file, regExp, captureGroup = 1) {
while (true) {
const match = regExp.exec(file.content);

if (match === null) {
break;
}

const path = match[captureGroup];
const pathAtIndex = file.content.indexOf(path);
const previousCharacter = file.content.charAt(pathAtIndex - 1);
Expand Down Expand Up @@ -141,32 +122,27 @@ function* getMatches(file, regExp, captureGroup = 1) {
* @param file a file object
* @returns a list of translation keys found in `file`.
*/


function extractMethodMatches(file) {
const methodRegExp = /(?:[$\s.:"'`+\(\[\{]t[cm]?)\(\s*?(["'`])((?:[^\\]|\\.)*?)\1/g;
return [...getMatches(file, methodRegExp, 2)];
}

function extractComponentMatches(file) {
const componentRegExp = /(?:(?:<|h\()(?:i18n|Translation))(?:.|\n)*?(?:\s(?:(?:key)?)path(?:=|: )("|'))((?:[^\\]|\\.)*?)\1/gi;
return [...getMatches(file, componentRegExp, 2)];
}

function extractDirectiveMatches(file) {
const directiveRegExp = /\bv-t(?:\.[\w-]+)?="'((?:[^\\]|\\.)*?)'"/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];
}, []);
} // This is a convenience function for users implementing in their own projects, and isn't used internally

}
// This is a convenience function for users implementing in their own projects, and isn't used internally
function parseVueFiles(vueFiles) {
return extractI18NItemsFromVueFiles(readVueFiles(vueFiles));
}
Expand All @@ -175,32 +151,26 @@ function readLanguageFiles(src) {
// Replace backslash path segments to make the path work with the glob package.
// https://github.com/Spittal/vue-i18n-extract/issues/159
const normalizedSrc = src.replace(/\\/g, '/');

if (!isValidGlob(normalizedSrc)) {
throw new Error(`languageFiles isn't a valid glob pattern.`);
}

const targetFiles = glob.sync(normalizedSrc);

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 = langPath.substring(langPath.lastIndexOf('.')).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.load(fs.readFileSync(langPath, 'utf8'));
} else {
langObj = eval(fs.readFileSync(langPath, 'utf8'));
}

const fileName = f.replace(process.cwd(), '.');
return {
path: f,
Expand All @@ -212,11 +182,9 @@ function readLanguageFiles(src) {
function extractI18NLanguageFromLanguageFiles(languageFiles, dot = Dot) {
return languageFiles.reduce((accumulator, file) => {
const language = file.fileName.substring(file.fileName.lastIndexOf('/') + 1, file.fileName.lastIndexOf('.'));

if (!accumulator[language]) {
accumulator[language] = [];
}

const flattenedObject = dot.dot(JSON.parse(file.content));
Object.keys(flattenedObject).forEach(key => {
accumulator[language].push({
Expand All @@ -233,7 +201,17 @@ function writeMissingToLanguageFiles(parsedLanguageFiles, missingKeys, dot = Dot
missingKeys.forEach(item => {
if (item.language && languageFile.fileName.includes(item.language) || !item.language) {
const addDefaultTranslation = noEmptyTranslation && (noEmptyTranslation === '*' || noEmptyTranslation === item.language);
dot.str(item.path, addDefaultTranslation ? item.path : missingTranslationString === 'null' ? null : missingTranslationString, languageFileContent);
let value = null;
if (addDefaultTranslation) {
value = item.path;
} else if (missingTranslationString === 'null') {
value = null;
} else if (missingTranslationString.includes('{{t}}')) {
value = missingTranslationString.replace('{{t}}', item.path);
} else {
value = missingTranslationString;
}
dot.str(item.path, value, languageFileContent);
}
});
writeLanguageFile(languageFile, languageFileContent);
Expand All @@ -250,12 +228,10 @@ function removeUnusedFromLanguageFiles(parsedLanguageFiles, unusedKeys, dot = Do
writeLanguageFile(languageFile, languageFileContent);
});
}

function writeLanguageFile(languageFile, newLanguageFileContent) {
const fileExtension = languageFile.fileName.substring(languageFile.fileName.lastIndexOf('.') + 1);
const filePath = languageFile.path;
const stringifiedContent = JSON.stringify(newLanguageFileContent, null, 2);

if (fileExtension === 'json') {
fs.writeFileSync(filePath, stringifiedContent);
} else if (fileExtension === 'js') {
Expand All @@ -267,9 +243,8 @@ function writeLanguageFile(languageFile, newLanguageFileContent) {
} else {
throw new Error(`Language filetype of ${fileExtension} not supported.`);
}
} // This is a convenience function for users implementing in their own projects, and isn't used internally


}
// This is a convenience function for users implementing in their own projects, and isn't used internally
function parselanguageFiles(languageFiles, dot = Dot) {
return extractI18NLanguageFromLanguageFiles(readLanguageFiles(languageFiles), dot);
}
Expand All @@ -281,31 +256,25 @@ function stripBounding(item) {
line: item.line
};
}

function mightBeDynamic(item) {
return item.path.includes('${') && !!item.previousCharacter.match(/`/g) && !!item.nextCharacter.match(/`/g);
} // Looping through the arays multiple times might not be the most effecient, but it's the easiest to read and debug. Which at this scale is an accepted trade-off.


}
// Looping through the arays multiple times might not be the most effecient, but it's the easiest to read and debug. Which at this scale is an accepted trade-off.
function extractI18NReport(vueItems, languageFiles, detect) {
const missingKeys = [];
const unusedKeys = [];
const maybeDynamicKeys = [];

if (detect.includes(DetectionType.Dynamic)) {
maybeDynamicKeys.push(...vueItems.filter(vueItem => mightBeDynamic(vueItem)).map(vueItem => stripBounding(vueItem)));
}

Object.keys(languageFiles).forEach(language => {
const languageItems = languageFiles[language];

if (detect.includes(DetectionType.Missing)) {
const missingKeysInLanguage = vueItems.filter(vueItem => !mightBeDynamic(vueItem)).filter(vueItem => !languageItems.some(languageItem => vueItem.path === languageItem.path)).map(vueItem => _extends({}, stripBounding(vueItem), {
language
}));
missingKeys.push(...missingKeysInLanguage);
}

if (detect.includes(DetectionType.Unused)) {
const unusedKeysInLanguage = languageItems.filter(languageItem => !vueItems.some(vueItem => languageItem.path === vueItem.path || languageItem.path.startsWith(vueItem.path + '.'))).map(languageItem => _extends({}, languageItem, {
language
Expand All @@ -327,7 +296,6 @@ async function writeReportToFile(report, writePath) {
reject(err);
return;
}

resolve();
});
});
Expand All @@ -351,11 +319,9 @@ async function createI18NReport(options) {
if (!languageFilesGlob) throw new Error('Required configuration languageFiles is missing.');
let issuesToDetect = Array.isArray(detect) ? detect : [detect];
const invalidDetectOptions = issuesToDetect.filter(item => !Object.values(DetectionType).includes(item));

if (invalidDetectOptions.length) {
throw new Error(`Invalid 'detect' value(s): ${invalidDetectOptions}`);
}

const dot = typeof separator === 'string' ? new Dot(separator) : Dot;
const vueFiles = readVueFiles(path.resolve(process.cwd(), vueFilesGlob));
const languageFiles = readLanguageFiles(path.resolve(process.cwd(), languageFilesGlob));
Expand All @@ -366,30 +332,24 @@ async function createI18NReport(options) {
if (report.missingKeys.length) console.info('\nMissing Keys'), console.table(report.missingKeys);
if (report.unusedKeys.length) console.info('\nUnused Keys'), console.table(report.unusedKeys);
if (report.maybeDynamicKeys.length) console.warn('\nSuspected Dynamic Keys Found\nvue-i18n-extract does not compile Vue templates and therefore can not infer the correct key for the following keys.'), console.table(report.maybeDynamicKeys);

if (output) {
await writeReportToFile(report, path.resolve(process.cwd(), output));
console.info(`\nThe report has been has been saved to ${output}`);
}

if (remove && report.unusedKeys.length) {
removeUnusedFromLanguageFiles(languageFiles, report.unusedKeys, dot);
console.info('\nThe unused keys have been removed from your language files.');
}

if (add && report.missingKeys.length) {
writeMissingToLanguageFiles(languageFiles, report.missingKeys, dot, noEmptyTranslation, missingTranslationString);
console.info('\nThe missing keys have been added to your language files.');
}

if (ci && report.missingKeys.length) {
throw new Error(`${report.missingKeys.length} missing keys found.`);
}

if (ci && report.unusedKeys.length) {
throw new Error(`${report.unusedKeys.length} unused keys found.`);
}

return report;
}

Expand Down
2 changes: 1 addition & 1 deletion dist/vue-i18n-extract.modern.mjs.map

Large diffs are not rendered by default.

Loading