diff --git a/docs/started.md b/docs/started.md index a7c48a84..4e7bafc9 100644 --- a/docs/started.md +++ b/docs/started.md @@ -35,6 +35,11 @@ module.export = { settings: { 'vue-i18n': { localeDir: './path/to/locales/*.json' // extention is glob formatting! + // or + // localeDir: { + // pattern: './path/to/locales/*.json', // extention is glob formatting! + // localeKey: 'file' // or 'key' + // } } } } @@ -42,6 +47,16 @@ module.export = { See [the rule list](../rules/) +### `settings['vue-i18n']` + +- `localeDir` ... You can specify a string or an object. + - String option ... A glob for specifying files that store localization messages of project. + - Object option + - `pattern` (`string`) ... A glob for specifying files that store localization messages of project. + - `localeKey` (`'file' | 'key'`) ... Specifies how to determine the locale for localization messages. + - `'file'` ... Determine the locale name from the filename. The resource file should only contain messages for that locale. Use this option if you use `vue-cli-plugin-i18n`. This option is also used when String option is specified. + - `'key'` ... Determine the locale name from the root key name of the file contents. The value of that key should only contain messages for that locale. Used when the resource file is in the format given to the `messages` option of the `VueI18n` constructor option. + ### Running ESLint from command line If you want to run `eslint` from command line, make sure you include the `.vue` and `.json` extension using [the `--ext` option](https://eslint.org/docs/user-guide/configuring#specifying-file-extensions-to-lint) or a glob pattern because ESLint targets only `.js` files by default. diff --git a/lib/rules/no-html-messages.js b/lib/rules/no-html-messages.js index aa01b074..feddd989 100644 --- a/lib/rules/no-html-messages.js +++ b/lib/rules/no-html-messages.js @@ -7,7 +7,6 @@ const { extname } = require('path') const parse5 = require('parse5') const { UNEXPECTED_ERROR_LOCATION, - findExistLocaleMessage, getLocaleMessages, extractJsonInfo, generateJsonAst @@ -55,7 +54,7 @@ function create (context) { } const localeMessages = getLocaleMessages(settings['vue-i18n'].localeDir) - const targetLocaleMessage = findExistLocaleMessage(filename, localeMessages) + const targetLocaleMessage = localeMessages.findExistLocaleMessage(filename) if (!targetLocaleMessage) { debug(`ignore ${filename} in no-html-messages`) return {} diff --git a/lib/rules/no-missing-keys.js b/lib/rules/no-missing-keys.js index 129c617d..e9e283fb 100644 --- a/lib/rules/no-missing-keys.js +++ b/lib/rules/no-missing-keys.js @@ -6,8 +6,7 @@ const { UNEXPECTED_ERROR_LOCATION, defineTemplateBodyVisitor, - getLocaleMessages, - findMissingsFromLocaleMessages + getLocaleMessages } = require('../utils/index') function create (context) { @@ -25,32 +24,32 @@ function create (context) { return defineTemplateBodyVisitor(context, { "VAttribute[directive=true][key.name='t']" (node) { - checkDirective(context, localeDir, localeMessages, node) + checkDirective(context, localeMessages, node) }, "VAttribute[directive=true][key.name.name='t']" (node) { - checkDirective(context, localeDir, localeMessages, node) + checkDirective(context, localeMessages, node) }, "VElement[name=i18n] > VStartTag > VAttribute[key.name='path']" (node) { - checkComponent(context, localeDir, localeMessages, node) + checkComponent(context, localeMessages, node) }, "VElement[name=i18n] > VStartTag > VAttribute[key.name.name='path']" (node) { - checkComponent(context, localeDir, localeMessages, node) + checkComponent(context, localeMessages, node) }, CallExpression (node) { - checkCallExpression(context, localeDir, localeMessages, node) + checkCallExpression(context, localeMessages, node) } }, { CallExpression (node) { - checkCallExpression(context, localeDir, localeMessages, node) + checkCallExpression(context, localeMessages, node) } }) } -function checkDirective (context, localeDir, localeMessages, node) { +function checkDirective (context, localeMessages, node) { if ((node.value && node.value.type === 'VExpressionContainer') && (node.value.expression && node.value.expression.type === 'Literal')) { const key = node.value.expression.value @@ -58,28 +57,28 @@ function checkDirective (context, localeDir, localeMessages, node) { // TODO: should be error return } - const missings = findMissingsFromLocaleMessages(localeMessages, key, localeDir) + const missings = localeMessages.findMissingPaths(key) if (missings.length) { - missings.forEach(missing => context.report({ node, ...missing })) + missings.forEach((data) => context.report({ node, messageId: 'missing', data })) } } } -function checkComponent (context, localeDir, localeMessages, node) { +function checkComponent (context, localeMessages, node) { if (node.value && node.value.type === 'VLiteral') { const key = node.value.value if (!key) { // TODO: should be error return } - const missings = findMissingsFromLocaleMessages(localeMessages, key, localeDir) + const missings = localeMessages.findMissingPaths(key) if (missings.length) { - missings.forEach(missing => context.report({ node, ...missing })) + missings.forEach((data) => context.report({ node, messageId: 'missing', data })) } } } -function checkCallExpression (context, localeDir, localeMessages, node) { +function checkCallExpression (context, localeMessages, node) { const funcName = (node.callee.type === 'MemberExpression' && node.callee.property.name) || node.callee.name if (!/^(\$t|t|\$tc|tc)$/.test(funcName) || !node.arguments || !node.arguments.length) { @@ -95,9 +94,9 @@ function checkCallExpression (context, localeDir, localeMessages, node) { return } - const missings = findMissingsFromLocaleMessages(localeMessages, key, localeDir) + const missings = localeMessages.findMissingPaths(key) if (missings.length) { - missings.forEach(missing => context.report({ node, ...missing })) + missings.forEach((data) => context.report({ node, messageId: 'missing', data })) } } @@ -110,7 +109,10 @@ module.exports = { recommended: true }, fixable: null, - schema: [] + schema: [], + messages: { + missing: "'{{path}}' does not exist in '{{locale}}'" + } }, create } diff --git a/lib/rules/no-unused-keys.js b/lib/rules/no-unused-keys.js index dbdfb077..5338c7d1 100644 --- a/lib/rules/no-unused-keys.js +++ b/lib/rules/no-unused-keys.js @@ -9,22 +9,38 @@ const flatten = require('flat') const collectKeys = require('../utils/collect-keys') const { UNEXPECTED_ERROR_LOCATION, - findExistLocaleMessage, getLocaleMessages, extractJsonInfo, generateJsonAst } = require('../utils/index') const debug = require('debug')('eslint-plugin-vue-i18n:no-unused-keys') +/** + * @typedef {import('../utils/locale-messages').LocaleMessage} LocaleMessage + */ + let usedLocaleMessageKeys = null // used locale message keys -function getUnusedKeys (context, json, usedkeys) { +/** + * @param {RuleContext} context + * @param {LocaleMessage} targetLocaleMessage + * @param {string} json + * @param {object} usedkeys + */ +function getUnusedKeys (context, targetLocaleMessage, json, usedkeys) { let unusedKeys = [] try { + let compareKeys = { ...usedkeys } + if (targetLocaleMessage.localeKey === 'key') { + compareKeys = targetLocaleMessage.locales.reduce((keys, locale) => { + keys[locale] = usedkeys + return keys + }, {}) + } const jsonValue = JSON.parse(json) const diffValue = jsonDiffPatch.diff( - flatten(usedkeys, { safe: true }), + flatten(compareKeys, { safe: true }), flatten(jsonValue, { safe: true }) ) const diffLocaleMessage = flatten(diffValue, { safe: true }) @@ -89,7 +105,7 @@ function create (context) { } const localeMessages = getLocaleMessages(settings['vue-i18n'].localeDir) - const targetLocaleMessage = findExistLocaleMessage(filename, localeMessages) + const targetLocaleMessage = localeMessages.findExistLocaleMessage(filename) if (!targetLocaleMessage) { debug(`ignore ${filename} in no-unused-keys`) return {} @@ -111,7 +127,7 @@ function create (context) { const ast = generateJsonAst(context, jsonString, jsonFilename) if (!ast) { return } - const unusedKeys = getUnusedKeys(context, jsonString, usedLocaleMessageKeys) + const unusedKeys = getUnusedKeys(context, targetLocaleMessage, jsonString, usedLocaleMessageKeys) if (!unusedKeys) { return } traverseJsonAstWithUnusedKeys(unusedKeys, ast, (fullpath, node) => { diff --git a/lib/utils/index.js b/lib/utils/index.js index 98aca376..bbd3b14e 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -7,9 +7,31 @@ const glob = require('glob') const { resolve } = require('path') const jsonAstParse = require('json-to-ast') +const { LocaleMessage, LocaleMessages } = require('./locale-messages') const UNEXPECTED_ERROR_LOCATION = { line: 1, column: 0 } +/** + * How to determine the locale for localization messages. + * - `'file'` ... Determine the locale name from the filename. The resource file should only contain messages for that locale. + * Use this option if you use `vue-cli-plugin-i18n`. This method is also used when String option is specified. + * - `'key'` ... Determine the locale name from the root key name of the file contents. The value of that key should only contain messages for that locale. + * Used when the resource file is in the format given to the `messages` option of the `VueI18n` constructor option. + * @typedef {'file' | 'key'} LocaleKeyType + */ +/** + * Type of `settings['vue-i18n'].localeDir` + * @typedef {SettingsVueI18nLocaleDirGlob | SettingsVueI18nLocaleDirObject} SettingsVueI18nLocaleDir + * @typedef {string} SettingsVueI18nLocaleDirGlob A glob for specifying files that store localization messages of project. + * @typedef {object} SettingsVueI18nLocaleDirObject Specifies a glob and messages format type. + * @property {string} pattern A glob for specifying files that store localization messages of project. + * @property {LocaleKeyType} localeKey Specifies how to determine the locale for localization messages. + */ +/** + * @typedef {import('./locale-messages').LocaleMessage} LocaleMessage + * @typedef {import('./locale-messages').LocaleMessages} LocaleMessages + */ + /** * Register the given visitor to parser services. * Borrow from GitHub `vuejs/eslint-plugin-vue` repo @@ -26,23 +48,29 @@ function defineTemplateBodyVisitor (context, templateBodyVisitor, scriptVisitor) return context.parserServices.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor) } -function findExistLocaleMessage (fullpath, localeMessages) { - return localeMessages.find(message => message.fullpath === fullpath) -} - -function loadLocaleMessages (pattern) { - const files = glob.sync(pattern) - return files.map(file => { - const path = resolve(process.cwd(), file) - const filename = file.replace(/^.*(\\|\/|:)/, '') - const messages = require(path) - return { fullpath: path, path: file, filename, messages } - }) +/** + * @param {SettingsVueI18nLocaleDir} localeDir + * @returns {LocaleMessages} + */ +function loadLocaleMessages (localeDir) { + if (typeof localeDir === 'string') { + return loadLocaleMessages({ pattern: localeDir, localeKey: 'file' }) + } else { + const files = glob.sync(localeDir.pattern) + return new LocaleMessages(files.map(file => { + const fullpath = resolve(process.cwd(), file) + return new LocaleMessage({ fullpath, path: file, localeKey: localeDir.localeKey || 'file' }) + })) + } } let localeMessages = null // locale messages let localeDir = null // locale dir +/** + * @param {SettingsVueI18nLocaleDir} localeDirectory + * @returns {LocaleMessages} + */ function getLocaleMessages (localeDirectory) { if (localeDir !== localeDirectory) { localeDir = localeDirectory @@ -53,27 +81,6 @@ function getLocaleMessages (localeDirectory) { return localeMessages } -function findMissingsFromLocaleMessages (localeMessages, key) { - const missings = [] - const paths = key.split('.') - localeMessages.forEach(localeMessage => { - const length = paths.length - let last = localeMessage.messages - let i = 0 - while (i < length) { - const value = last && last[paths[i]] - if (value === undefined) { - missings.push({ - message: `'${key}' does not exist` - }) - } - last = value - i++ - } - }) - return missings -} - function extractJsonInfo (context, node) { try { const [str, filename] = node.comments @@ -110,8 +117,6 @@ module.exports = { UNEXPECTED_ERROR_LOCATION, defineTemplateBodyVisitor, getLocaleMessages, - findMissingsFromLocaleMessages, - findExistLocaleMessage, extractJsonInfo, generateJsonAst } diff --git a/lib/utils/locale-messages.js b/lib/utils/locale-messages.js new file mode 100644 index 00000000..3f915d6f --- /dev/null +++ b/lib/utils/locale-messages.js @@ -0,0 +1,153 @@ +/** + * @fileoverview Classes that acquires and manages localization messages + * @author Yosuke Ota + */ +'use strict' + +/** + * @typedef {import('./index').LocaleKeyType} LocaleKeyType + * @typedef {LocaleMessage} LocaleMessage + * @typedef {LocaleMessages} LocaleMessages + */ +/** + * The localization message class + */ +class LocaleMessage { + /** + * @param {object} arg + * @param {string} arg.fullpath Absolute path. + * @param {string} arg.path Relative path. + * @param {LocaleKeyType} arg.localeKey Specifies how to determine the locale for localization messages. + */ + constructor ({ fullpath, path, localeKey }) { + this.fullpath = fullpath + this.path = path + /** @type {LocaleKeyType[]} Specifies how to determine the locale for localization messages. */ + this.localeKey = localeKey + /** @type {string} The localization messages file name. */ + this.file = path.replace(/^.*(\\|\/|:)/, '') + } + + /** + * @returns {object} The localization messages object. + */ + get messages () { + return this._messages || (this._messages = require(this.fullpath)) + } + /** + * @returns {string[]} Array of locales. + */ + get locales () { + if (this._locales) { + return this._locales + } + if (this.localeKey === 'file') { + // see https://github.com/kazupon/vue-cli-plugin-i18n/blob/e9519235a454db52fdafcd0517ce6607821ef0b4/generator/templates/js/src/i18n.js#L10 + const matched = this.file.match(/([A-Za-z0-9-_]+)\./i) + return (this._locales = [matched && matched[1] || this.file]) + } else if (this.localeKey === 'key') { + return (this._locales = Object.keys(this.messages)) + } + return (this._locales = []) + } + + findMissingPath (locale, key) { + const paths = key.split('.') + const length = paths.length + let last = this._getMessagesFromLocale(locale) + let i = 0 + while (i < length) { + const value = last && last[paths[i]] + if (value == null) { + return paths.slice(0, i + 1).join('.') + } + last = value + i++ + } + return null + } + + /** + * Check if the message with the given key exists. + * @param {string} locale The locale name + * @param {string} key The given key to check + * @returns {boolean} + */ + hasMessage (locale, key) { + return this.getMessage(locale, key) != null + } + + /** + * Gets the message for the given key. + * @param {string} locale The locale name + * @param {string} key The given key + * @returns {any} The message for the given key. `null` if the message is missing. + */ + getMessage (locale, key) { + const paths = key.split('.') + const length = paths.length + let last = this._getMessagesFromLocale(locale) + let i = 0 + while (i < length) { + const value = last && last[paths[i]] + if (value == null) { + return null + } + last = value + i++ + } + return last + } + + _getMessagesFromLocale (locale) { + if (this.localeKey === 'file') { + return this.messages + } + if (this.localeKey === 'key') { + return this.messages[locale] + } + } +} + +/** + * The localization messages class + */ +class LocaleMessages { + /** + * @param {LocaleMessage[]} localeMessages + */ + constructor (localeMessages) { + this.localeMessages = localeMessages + } + + /** + * Finds and gets the localization message for the given fullpath. + * @param {string} fullpath + * @returns {LocaleMessage} + */ + findExistLocaleMessage (fullpath) { + return this.localeMessages.find(message => message.fullpath === fullpath) + } + + /** + * Finds the paths that does not exist in the localization message resources. + * @param {string} key + */ + findMissingPaths (key) { + const missings = [] + this.localeMessages.forEach(localeMessage => { + localeMessage.locales.forEach(locale => { + const missingPath = localeMessage.findMissingPath(locale, key) + if (missingPath) { + missings.push({ path: missingPath, locale }) + } + }) + }) + return missings.sort(({ locale: localeA }, { locale: localeB },) => localeA > localeB ? 1 : localeA < localeB ? -1 : 0) + } +} + +module.exports = { + LocaleMessage, + LocaleMessages +} diff --git a/package.json b/package.json index 3cf17fe7..df86f921 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "release:prepare": "shipjs prepare", "release:trigger": "shipjs trigger", "test": "mocha ./tests/**/*.js", + "test:debug": "mocha --inspect \"./tests/**/*.js\"", "test:coverage": "nyc mocha ./tests/**/*.js", "test:integrations": "mocha ./tests-integrations/*.js --timeout 60000" } diff --git a/tests-integrations/config-recommended/.vscode/settings.json b/tests-integrations/config-recommended/.vscode/settings.json new file mode 100644 index 00000000..b9d096c7 --- /dev/null +++ b/tests-integrations/config-recommended/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "eslint.validate": [ + "javascript", + "vue", + "json" + ] +} diff --git a/tests/fixtures/no-missing-keys/constructor-option-format/locales/index.json b/tests/fixtures/no-missing-keys/constructor-option-format/locales/index.json new file mode 100644 index 00000000..5699f357 --- /dev/null +++ b/tests/fixtures/no-missing-keys/constructor-option-format/locales/index.json @@ -0,0 +1,28 @@ +{ + "en": { + "hello": "hello world", + "messages": { + "hello": "hi DIO!", + "link": "@:message.hello", + "nested": { + "hello": "hi jojo!" + } + }, + "hello_dio": "hello underscore DIO!", + "hello {name}": "hello {name}!", + "hello-dio": "hello hyphen DIO!" + }, + "ja": { + "hello": "ハローワールド", + "messages": { + "hello": "こんにちは、DIO!", + "link": "@:message.hello", + "nested": { + "hello": "こんにちは、ジョジョ!" + } + }, + "hello_dio": "こんにちは、アンダースコア DIO!", + "hello {name}": "こんにちは、{name}!", + "hello-dio": "こんにちは、ハイフン DIO!" + } +} diff --git a/tests/fixtures/no-missing-keys/src/App.vue b/tests/fixtures/no-missing-keys/constructor-option-format/src/App.vue similarity index 100% rename from tests/fixtures/no-missing-keys/src/App.vue rename to tests/fixtures/no-missing-keys/constructor-option-format/src/App.vue diff --git a/tests/fixtures/no-missing-keys/src/main.js b/tests/fixtures/no-missing-keys/constructor-option-format/src/main.js similarity index 100% rename from tests/fixtures/no-missing-keys/src/main.js rename to tests/fixtures/no-missing-keys/constructor-option-format/src/main.js diff --git a/tests/fixtures/no-missing-keys/locales/en.json b/tests/fixtures/no-missing-keys/vue-cli-format/locales/en.json similarity index 100% rename from tests/fixtures/no-missing-keys/locales/en.json rename to tests/fixtures/no-missing-keys/vue-cli-format/locales/en.json diff --git a/tests/fixtures/no-missing-keys/locales/ja.json b/tests/fixtures/no-missing-keys/vue-cli-format/locales/ja.json similarity index 100% rename from tests/fixtures/no-missing-keys/locales/ja.json rename to tests/fixtures/no-missing-keys/vue-cli-format/locales/ja.json diff --git a/tests/fixtures/no-unused-keys/invalid/src/App.vue b/tests/fixtures/no-missing-keys/vue-cli-format/src/App.vue similarity index 100% rename from tests/fixtures/no-unused-keys/invalid/src/App.vue rename to tests/fixtures/no-missing-keys/vue-cli-format/src/App.vue diff --git a/tests/fixtures/no-unused-keys/invalid/src/main.js b/tests/fixtures/no-missing-keys/vue-cli-format/src/main.js similarity index 100% rename from tests/fixtures/no-unused-keys/invalid/src/main.js rename to tests/fixtures/no-missing-keys/vue-cli-format/src/main.js diff --git a/tests/fixtures/no-unused-keys/invalid/constructor-option-format/locales/index.json b/tests/fixtures/no-unused-keys/invalid/constructor-option-format/locales/index.json new file mode 100644 index 00000000..5699f357 --- /dev/null +++ b/tests/fixtures/no-unused-keys/invalid/constructor-option-format/locales/index.json @@ -0,0 +1,28 @@ +{ + "en": { + "hello": "hello world", + "messages": { + "hello": "hi DIO!", + "link": "@:message.hello", + "nested": { + "hello": "hi jojo!" + } + }, + "hello_dio": "hello underscore DIO!", + "hello {name}": "hello {name}!", + "hello-dio": "hello hyphen DIO!" + }, + "ja": { + "hello": "ハローワールド", + "messages": { + "hello": "こんにちは、DIO!", + "link": "@:message.hello", + "nested": { + "hello": "こんにちは、ジョジョ!" + } + }, + "hello_dio": "こんにちは、アンダースコア DIO!", + "hello {name}": "こんにちは、{name}!", + "hello-dio": "こんにちは、ハイフン DIO!" + } +} diff --git a/tests/fixtures/no-unused-keys/valid/src/App.vue b/tests/fixtures/no-unused-keys/invalid/constructor-option-format/src/App.vue similarity index 100% rename from tests/fixtures/no-unused-keys/valid/src/App.vue rename to tests/fixtures/no-unused-keys/invalid/constructor-option-format/src/App.vue diff --git a/tests/fixtures/no-unused-keys/valid/src/main.js b/tests/fixtures/no-unused-keys/invalid/constructor-option-format/src/main.js similarity index 100% rename from tests/fixtures/no-unused-keys/valid/src/main.js rename to tests/fixtures/no-unused-keys/invalid/constructor-option-format/src/main.js diff --git a/tests/fixtures/no-unused-keys/invalid/locales/en.json b/tests/fixtures/no-unused-keys/invalid/vue-cli-format/locales/en.json similarity index 100% rename from tests/fixtures/no-unused-keys/invalid/locales/en.json rename to tests/fixtures/no-unused-keys/invalid/vue-cli-format/locales/en.json diff --git a/tests/fixtures/no-unused-keys/invalid/locales/ja.json b/tests/fixtures/no-unused-keys/invalid/vue-cli-format/locales/ja.json similarity index 100% rename from tests/fixtures/no-unused-keys/invalid/locales/ja.json rename to tests/fixtures/no-unused-keys/invalid/vue-cli-format/locales/ja.json diff --git a/tests/fixtures/no-unused-keys/invalid/vue-cli-format/src/App.vue b/tests/fixtures/no-unused-keys/invalid/vue-cli-format/src/App.vue new file mode 100644 index 00000000..a68c3d32 --- /dev/null +++ b/tests/fixtures/no-unused-keys/invalid/vue-cli-format/src/App.vue @@ -0,0 +1,14 @@ + + + diff --git a/tests/fixtures/no-unused-keys/invalid/vue-cli-format/src/main.js b/tests/fixtures/no-unused-keys/invalid/vue-cli-format/src/main.js new file mode 100644 index 00000000..8140aeca --- /dev/null +++ b/tests/fixtures/no-unused-keys/invalid/vue-cli-format/src/main.js @@ -0,0 +1,2 @@ +const $t = () => {} +$t('hello') diff --git a/tests/fixtures/no-unused-keys/valid/constructor-option-format/locales/index.json b/tests/fixtures/no-unused-keys/valid/constructor-option-format/locales/index.json new file mode 100644 index 00000000..31ec4785 --- /dev/null +++ b/tests/fixtures/no-unused-keys/valid/constructor-option-format/locales/index.json @@ -0,0 +1,18 @@ +{ + "en": { + "hello": "hello world", + "messages": { + "hello": "hi DIO!" + }, + "hello_dio": "hello underscore DIO!", + "hello {name}": "hello {name}!" + }, + "ja": { + "hello": "ハローワールド", + "messages": { + "hello": "こんにちは、DIO!" + }, + "hello_dio": "こんにちは、アンダースコア DIO!", + "hello {name}": "こんにちは、{name}!" + } +} diff --git a/tests/fixtures/no-unused-keys/valid/constructor-option-format/src/App.vue b/tests/fixtures/no-unused-keys/valid/constructor-option-format/src/App.vue new file mode 100644 index 00000000..a68c3d32 --- /dev/null +++ b/tests/fixtures/no-unused-keys/valid/constructor-option-format/src/App.vue @@ -0,0 +1,14 @@ + + + diff --git a/tests/fixtures/no-unused-keys/valid/constructor-option-format/src/main.js b/tests/fixtures/no-unused-keys/valid/constructor-option-format/src/main.js new file mode 100644 index 00000000..8140aeca --- /dev/null +++ b/tests/fixtures/no-unused-keys/valid/constructor-option-format/src/main.js @@ -0,0 +1,2 @@ +const $t = () => {} +$t('hello') diff --git a/tests/fixtures/no-unused-keys/valid/locales/en.json b/tests/fixtures/no-unused-keys/valid/vue-cli-format/locales/en.json similarity index 100% rename from tests/fixtures/no-unused-keys/valid/locales/en.json rename to tests/fixtures/no-unused-keys/valid/vue-cli-format/locales/en.json diff --git a/tests/fixtures/no-unused-keys/valid/locales/ja.json b/tests/fixtures/no-unused-keys/valid/vue-cli-format/locales/ja.json similarity index 100% rename from tests/fixtures/no-unused-keys/valid/locales/ja.json rename to tests/fixtures/no-unused-keys/valid/vue-cli-format/locales/ja.json diff --git a/tests/fixtures/no-unused-keys/valid/vue-cli-format/src/App.vue b/tests/fixtures/no-unused-keys/valid/vue-cli-format/src/App.vue new file mode 100644 index 00000000..a68c3d32 --- /dev/null +++ b/tests/fixtures/no-unused-keys/valid/vue-cli-format/src/App.vue @@ -0,0 +1,14 @@ + + + diff --git a/tests/fixtures/no-unused-keys/valid/vue-cli-format/src/main.js b/tests/fixtures/no-unused-keys/valid/vue-cli-format/src/main.js new file mode 100644 index 00000000..8140aeca --- /dev/null +++ b/tests/fixtures/no-unused-keys/valid/vue-cli-format/src/main.js @@ -0,0 +1,2 @@ +const $t = () => {} +$t('hello') diff --git a/tests/lib/rules/no-missing-keys.js b/tests/lib/rules/no-missing-keys.js index 38b5f90c..fe512f76 100644 --- a/tests/lib/rules/no-missing-keys.js +++ b/tests/lib/rules/no-missing-keys.js @@ -6,12 +6,21 @@ const RuleTester = require('eslint').RuleTester const rule = require('../../../lib/rules/no-missing-keys') -const baseDir = './tests/fixtures/no-missing-keys/locales' +const localeDirs = [ + './tests/fixtures/no-missing-keys/vue-cli-format/locales/*.json', + { pattern: './tests/fixtures/no-missing-keys/constructor-option-format/locales/*.json', localeKey: 'key' } +] -const settings = { - 'vue-i18n': { - localeDir: `${baseDir}/*.json` +function buildTestsForLocales (testcases) { + const result = [] + for (const testcase of testcases) { + for (const localeDir of localeDirs) { + result.push({ ...testcase, settings: { + 'vue-i18n': { localeDir } + }}) + } } + return result } const tester = new RuleTester({ @@ -20,103 +29,94 @@ const tester = new RuleTester({ }) tester.run('no-missing-keys', rule, { - valid: [{ + valid: buildTestsForLocales([{ // basic key - settings, code: `$t('hello')` }, { // nested key - settings, code: `t('messages.nested.hello')` }, { // linked key - settings, code: `$tc('messages.hello.link')` }, { // hypened key - settings, code: `tc('hello-dio')` }, { // key like the message - settings, code: `$t('hello {name}')` }, { // instance member - settings, code: `i18n.t('hello {name}')` }, { // identifier - settings, code: `$t(key)` }, { // using mustaches in template block - settings, code: `` }, { // using custom directive in template block - settings, code: `` - }], + }]), - invalid: [{ + invalid: [...buildTestsForLocales([{ // basic - settings, code: `$t('missing')`, errors: [ - `'missing' does not exist`, - `'missing' does not exist` + `'missing' does not exist in 'en'`, + `'missing' does not exist in 'ja'` ] }, { // using mustaches in template block - settings, code: ``, errors: [ - `'missing' does not exist`, - `'missing' does not exist` + `'missing' does not exist in 'en'`, + `'missing' does not exist in 'ja'` ] }, { // using custom directive in template block - settings, code: ``, errors: [ - `'missing' does not exist`, - `'missing' does not exist` + `'missing' does not exist in 'en'`, + `'missing' does not exist in 'ja'` ] }, { // using functional component in template block - settings, code: ``, errors: [ - `'missing' does not exist`, - `'missing' does not exist` + `'missing' does not exist in 'en'`, + `'missing' does not exist in 'ja'` ] }, { - // settings.vue-i18n.localeDir' error - code: `$t('missing')`, + // nested basic + code: `$t('missing.path')`, errors: [ - `You need to set 'localeDir' at 'settings. See the 'eslint-plugin-vue-i18n documentation` + `'missing' does not exist in 'en'`, + `'missing' does not exist in 'ja'` ] }, { - // nested basic - settings, - code: `$t('missing.path')`, + // nested missing + code: `$t('messages.missing')`, + errors: [ + `'messages.missing' does not exist in 'en'`, + `'messages.missing' does not exist in 'ja'` + ] + }]), { + // settings.vue-i18n.localeDir' error + code: `$t('missing')`, errors: [ - `'missing.path' does not exist`, - `'missing.path' does not exist`, - `'missing.path' does not exist`, - `'missing.path' does not exist` + `You need to set 'localeDir' at 'settings. See the 'eslint-plugin-vue-i18n documentation` ] }] }) diff --git a/tests/lib/rules/no-unused-keys.js b/tests/lib/rules/no-unused-keys.js index 468f0431..e30621ad 100644 --- a/tests/lib/rules/no-unused-keys.js +++ b/tests/lib/rules/no-unused-keys.js @@ -45,7 +45,7 @@ describe('no-unused-keys', () => { }) const messages = linter.executeOnFiles(['.']) - assert.equal(messages.errorCount, 4) + assert.equal(messages.errorCount, 6) messages.results.map(result => { return result.messages .filter(message => message.ruleId === '@intlify/vue-i18n/no-unused-keys') @@ -62,7 +62,33 @@ describe('no-unused-keys', () => { baseConfig: { settings: { 'vue-i18n': { - localeDir: `./valid/locales/*.json` + localeDir: `./valid/vue-cli-format/locales/*.json` + } + } + }, + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2015 + }, + plugins: ['@intlify/vue-i18n'], + rules: { + '@intlify/vue-i18n/no-unused-keys': ['error', { + src: resolve(__dirname, '../../fixtures/no-unused-keys/valid/vue-cli-format') + }] + }, + extensions: ['.js', '.vue', '.json'] + }) + + const messages = linter.executeOnFiles(['.']) + assert.equal(messages.errorCount, 0) + }) + + it('should be not detected unsued keys for constructor-option-format', () => { + const linter = new CLIEngine({ + baseConfig: { + settings: { + 'vue-i18n': { + localeDir: { pattern: `./valid/constructor-option-format/locales/*.json`, localeKey: 'key' } } } }, @@ -73,7 +99,7 @@ describe('no-unused-keys', () => { plugins: ['@intlify/vue-i18n'], rules: { '@intlify/vue-i18n/no-unused-keys': ['error', { - src: resolve(__dirname, '../../fixtures/no-unused-keys/valid') + src: resolve(__dirname, '../../fixtures/no-unused-keys/valid/constructor-option-format') }] }, extensions: ['.js', '.vue', '.json'] @@ -90,7 +116,42 @@ describe('no-unused-keys', () => { baseConfig: { settings: { 'vue-i18n': { - localeDir: `./invalid/locales/*.json` + localeDir: `./invalid/vue-cli-format/locales/*.json` + } + } + }, + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2015 + }, + plugins: ['@intlify/vue-i18n'], + rules: { + '@intlify/vue-i18n/no-unused-keys': 'error' + }, + extensions: ['.js', '.vue', '.json'] + }) + + const messages = linter.executeOnFiles(['.']) + assert.equal(messages.errorCount, 6) + + function checkRuleId (path) { + const fullPath = resolve(__dirname, path) + const [result] = messages.results + .filter(result => result.filePath === fullPath) + result.messages.forEach(message => { + assert.equal(message.ruleId, '@intlify/vue-i18n/no-unused-keys') + }) + } + checkRuleId('../../fixtures/no-unused-keys/invalid/vue-cli-format/locales/en.json') + checkRuleId('../../fixtures/no-unused-keys/invalid/vue-cli-format/locales/ja.json') + }) + + it('should be detected unsued keys for constructor-option-format', () => { + const linter = new CLIEngine({ + baseConfig: { + settings: { + 'vue-i18n': { + localeDir: { pattern: `./invalid/constructor-option-format/locales/*.json`, localeKey: 'key' } } } }, @@ -116,8 +177,7 @@ describe('no-unused-keys', () => { assert.equal(message.ruleId, '@intlify/vue-i18n/no-unused-keys') }) } - checkRuleId('../../fixtures/no-unused-keys/invalid/locales/en.json') - checkRuleId('../../fixtures/no-unused-keys/invalid/locales/ja.json') + checkRuleId('../../fixtures/no-unused-keys/invalid/constructor-option-format/locales/index.json') }) }) })