-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from lifeomic/CON-1959
CON-1959: add default-value rule
- Loading branch information
Showing
7 changed files
with
296 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,31 @@ | ||
This repository contains an ESLint plugin for validating usage of [`18next`](https://github.com/i18next/i18next). | ||
|
||
## Installation | ||
|
||
``` | ||
yarn add -D @lifeomic/eslint-plugin-18next | ||
``` | ||
|
||
## Usage | ||
|
||
```javascript | ||
// .eslintrc.js | ||
module.exports = { | ||
plugins: ['@lifeomic/i18next'], | ||
rules: { | ||
'i18next/default-value': [ | ||
'error', | ||
{ | ||
/* optional options object */ | ||
}, | ||
], | ||
}, | ||
}; | ||
``` | ||
|
||
## Rule Options | ||
|
||
#### `default-value` | ||
|
||
- `translateFunctionNames`: an array of translation function names to validate. Default is `['t']` | ||
- `allowKeyOnly`: whether to allow e.g. `t('just-the-key')` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { RuleTester } from 'eslint'; | ||
import { defaultValueRule, Errors } from './default-value'; | ||
|
||
const tester = new RuleTester(); | ||
|
||
tester.run('default-value', defaultValueRule, { | ||
valid: [ | ||
{ | ||
code: ` | ||
t('key', { defaultValue: 'some long value' }) | ||
`, | ||
}, | ||
{ | ||
code: ` | ||
i18n.t('key', { defaultValue: 'some long value' }) | ||
`, | ||
}, | ||
{ | ||
code: ` | ||
translate('key', { defaultValue: 'some long value' }) | ||
`, | ||
options: [{ translateFunctionNames: ['translate'] }], | ||
}, | ||
{ | ||
code: ` | ||
translate('key', { defaultValue: 'some {{long}} value' }) | ||
`, | ||
options: [{ translateFunctionNames: ['notTranslate'] }], | ||
}, | ||
{ | ||
code: ` | ||
t('key', { | ||
defaultValue: 'some {{long}} value', | ||
long: 'something' | ||
}) | ||
`, | ||
}, | ||
{ | ||
code: ` | ||
t('key') | ||
`, | ||
options: [{ allowKeyOnly: true }], | ||
}, | ||
{ | ||
code: ` | ||
someOtherFunction() | ||
`, | ||
}, | ||
], | ||
invalid: [ | ||
{ | ||
code: ` | ||
t('key') | ||
`, | ||
errors: [Errors.twoArguments], | ||
}, | ||
{ | ||
code: ` | ||
t('key', 'bogus') | ||
`, | ||
errors: [Errors.optionsObject], | ||
}, | ||
{ | ||
code: ` | ||
t('key', { notDefaultValue: 'something' }) | ||
`, | ||
errors: [Errors.defaultValuePresence], | ||
}, | ||
{ | ||
code: ` | ||
t('key', { defaultValue: true }) | ||
`, | ||
errors: [Errors.defaultValuePresence], | ||
}, | ||
{ | ||
code: ` | ||
t('key', { defaultValue: 'some {{long}} value' }) | ||
`, | ||
errors: [Errors.missingVariable('long')], | ||
}, | ||
{ | ||
code: ` | ||
translate('key', { | ||
defaultValue: 'some {{long }} value' | ||
}) | ||
`, | ||
options: [{ translateFunctionNames: ['translate'] }], | ||
errors: [Errors.missingVariable('long')], | ||
}, | ||
{ | ||
code: ` | ||
translate('key', { | ||
defaultValue: 'some {{long}} value' | ||
}); | ||
t('key', { | ||
defaultValue: 'some {{short}} value' | ||
}) | ||
`, | ||
options: [{ translateFunctionNames: ['translate', 't'] }], | ||
errors: [Errors.missingVariable('long'), Errors.missingVariable('short')], | ||
}, | ||
], | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
import { Rule } from 'eslint'; | ||
import { ObjectExpression } from 'estree'; | ||
import { findPropertyOnNode, isFunctionCallExpression } from '../util'; | ||
|
||
export const Errors = { | ||
twoArguments: 'Translate function should have two arguments', | ||
optionsObject: 'Translate function options should be an object', | ||
defaultValuePresence: | ||
'Translate function defaultValue must a string property on the second argument', | ||
missingVariable: (named: string) => | ||
`Missing "${named}" in translate variables`, | ||
}; | ||
|
||
const findDefaultValueOnObject = (node: ObjectExpression) => { | ||
const prop = findPropertyOnNode(node, { named: 'defaultValue' }); | ||
|
||
if (prop?.value.type === 'Literal' && typeof prop.value.value === 'string') { | ||
const variableNames = prop.value.value | ||
.match(/{{[ a-zA-Z0-9]+}}/g) | ||
?.map((value) => value.replace('{{', '').replace('}}', '').trim()); | ||
|
||
return { | ||
value: prop.value.value, | ||
variableNames: variableNames ?? [], | ||
}; | ||
} | ||
}; | ||
|
||
export type DefaultValueOptions = { | ||
translateFunctionNames: string[]; | ||
allowKeyOnly: boolean; | ||
}; | ||
|
||
const parseOptions = (context: Rule.RuleContext): DefaultValueOptions => { | ||
const DEFAULT: DefaultValueOptions = { | ||
translateFunctionNames: ['t'], | ||
allowKeyOnly: false, | ||
}; | ||
|
||
if (!context.options.length) { | ||
return DEFAULT; | ||
} | ||
|
||
return { | ||
...DEFAULT, | ||
...context.options[0], | ||
}; | ||
}; | ||
|
||
export const defaultValueRule: Rule.RuleModule = { | ||
meta: { | ||
schema: [ | ||
{ | ||
type: 'object', | ||
additionalProperties: true, | ||
properties: { | ||
translateFunctionNames: { | ||
type: 'array', | ||
items: { type: 'string' }, | ||
}, | ||
allowKeyOnly: { | ||
type: 'boolean', | ||
}, | ||
}, | ||
}, | ||
], | ||
}, | ||
create: (context) => { | ||
const options = parseOptions(context); | ||
|
||
return { | ||
CallExpression: (node) => { | ||
if ( | ||
!isFunctionCallExpression(node, { | ||
named: options.translateFunctionNames, | ||
}) | ||
) { | ||
return; | ||
} | ||
|
||
if (node.arguments.length < 2) { | ||
if (options.allowKeyOnly && node.arguments.length === 1) { | ||
return; | ||
} | ||
context.report({ | ||
node, | ||
message: Errors.twoArguments, | ||
}); | ||
return; | ||
} | ||
|
||
const secondArg = node.arguments[1]; | ||
|
||
if (secondArg.type !== 'ObjectExpression') { | ||
context.report({ | ||
node, | ||
message: Errors.optionsObject, | ||
}); | ||
return; | ||
} | ||
|
||
const defaultValue = findDefaultValueOnObject(secondArg); | ||
if (!defaultValue) { | ||
context.report({ | ||
node, | ||
message: Errors.defaultValuePresence, | ||
}); | ||
return; | ||
} | ||
|
||
for (const variable of defaultValue.variableNames) { | ||
const prop = findPropertyOnNode(secondArg, { named: variable }); | ||
if (!prop) { | ||
context.report({ | ||
node, | ||
message: Errors.missingVariable(variable), | ||
}); | ||
return; | ||
} | ||
} | ||
}, | ||
}; | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './default-value'; |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,7 @@ | ||
export const placeholder = 'placeholder'; | ||
import { defaultValueRule } from './default-value/default-value'; | ||
|
||
export default { | ||
rules: { | ||
'default-value': defaultValueRule, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { CallExpression, ObjectExpression } from 'estree'; | ||
|
||
export const isFunctionCallExpression = ( | ||
node: CallExpression, | ||
options: { named: string[] }, | ||
) => { | ||
return ( | ||
(node.callee.type === 'MemberExpression' && | ||
options.named.includes((node.callee.property as any).name)) || | ||
(node.callee.type === 'Identifier' && | ||
options.named.includes(node.callee.name)) | ||
); | ||
}; | ||
|
||
export const findPropertyOnNode = ( | ||
{ properties }: ObjectExpression, | ||
options: { | ||
named: string; | ||
}, | ||
) => { | ||
for (const prop of properties) { | ||
if ( | ||
prop.type === 'Property' && | ||
prop.key.type === 'Identifier' && | ||
prop.key.name === options.named | ||
) { | ||
return prop; | ||
} | ||
} | ||
}; |