Skip to content
Merged
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
15 changes: 15 additions & 0 deletions docs/started.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,28 @@ 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'
// }
}
}
}
```

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.
Expand Down
3 changes: 1 addition & 2 deletions lib/rules/no-html-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const { extname } = require('path')
const parse5 = require('parse5')
const {
UNEXPECTED_ERROR_LOCATION,
findExistLocaleMessage,
getLocaleMessages,
extractJsonInfo,
generateJsonAst
Expand Down Expand Up @@ -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 {}
Expand Down
38 changes: 20 additions & 18 deletions lib/rules/no-missing-keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
const {
UNEXPECTED_ERROR_LOCATION,
defineTemplateBodyVisitor,
getLocaleMessages,
findMissingsFromLocaleMessages
getLocaleMessages
} = require('../utils/index')

function create (context) {
Expand All @@ -25,61 +24,61 @@ 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
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 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) {
Expand All @@ -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 }))
}
}

Expand All @@ -110,7 +109,10 @@ module.exports = {
recommended: true
},
fixable: null,
schema: []
schema: [],
messages: {
missing: "'{{path}}' does not exist in '{{locale}}'"
}
},
create
}
26 changes: 21 additions & 5 deletions lib/rules/no-unused-keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -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 {}
Expand All @@ -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) => {
Expand Down
75 changes: 40 additions & 35 deletions lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -110,8 +117,6 @@ module.exports = {
UNEXPECTED_ERROR_LOCATION,
defineTemplateBodyVisitor,
getLocaleMessages,
findMissingsFromLocaleMessages,
findExistLocaleMessage,
extractJsonInfo,
generateJsonAst
}
Loading