Skip to content

Commit

Permalink
feat(i18n): add interpolation rule (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
D34THWINGS authored and Florent Dubost committed Jul 11, 2017
1 parent e3201a2 commit d088dc6
Show file tree
Hide file tree
Showing 20 changed files with 3,956 additions and 179 deletions.
11 changes: 11 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
root = true

[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
node_modules/
yarn.lock

lib/
4 changes: 4 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
yarn.lock
.babelrc
.eslintrc
tests/
10 changes: 10 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
language: node_js
node_js:
- "7"

cache: yarn

script:
- yarn run lint
- yarn run test
- yarn run build
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ yarn mversion -- patch -m

## Rules

* i18n/no-unknown-key: Verify that all translation keys you use are present in your primary translation files.
* i18n/no-unknown-key-secondary-langs: Same as the previous one. Allow you to have a different error level for secondary languages.
* i18n/no-text-as-children: Verify that you have no text children in your react code.
* **i18n/no-unknown-key**: Verify that all translation keys you use are present in your primary translation files.
* **i18n/no-unknown-key-secondary-langs**: Same as the previous one. Allow you to have a different error level for secondary languages.
* **i18n/no-text-as-children**: Verify that you have no text children in your react code.
* **i18n/interpolation-data**: Checks for usage of keys containing string interpolation, if translate function is called without
interpolation data it will show an error. Also if interpolation data is given and key doesn't contain interpolation it will also
show an error. `interpolationPattern` option is required to match interpolation in your translation file.

## Config

Expand All @@ -43,7 +46,8 @@ You have to add the following lines in your `.eslintrc` file to configure this p
"rules": {
"i18n/no-unknown-key": "error",
"i18n/no-unknown-key-secondary-langs": "warn",
"i18n/no-text-as-children": "error"
"i18n/no-text-as-children": "error",
"i18n/interpolation-data": ["error", { "interpolationPattern": "\\{\\.+\\}" }]
},
// The plugin needs jsx feature to be on for 'no-text-as-children' rule
"parserOptions": {
Expand Down Expand Up @@ -71,9 +75,11 @@ You have to add the following lines in your `.eslintrc` file to configure this p
// Name of your translate function
"functionName": "t",
// If you want to ignore specific files
"ignoreFiles": "spec.js",
"ignoreFiles": "**/*.spec.js",
// If you have pluralization
"pluralizedKeys": ["one", "other"]
"pluralizedKeys": ["one", "other"],
// TTL of the translations file caching (defaults to 500ms)
"translationsCacheTTL": 300
}
}
```
11 changes: 8 additions & 3 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
'use strict';

var noUnknownKey = require('./rules/no-unknown-key');
var noTextAsChildren = require('./rules/no-text-as-children');
var interpolationData = require('./rules/interpolation-data');

module.exports = {
rules: {
'no-unknown-key': require('./rules/no-unknown-key')('principalLangs'),
'no-unknown-key-secondary-langs': require('./rules/no-unknown-key')('secondaryLangs'),
'no-text-as-children': require('./rules/no-text-as-children')
'no-unknown-key': noUnknownKey('principalLangs'),
'no-unknown-key-secondary-langs': noUnknownKey('secondaryLangs'),
'no-text-as-children': noTextAsChildren,
'interpolation-data': interpolationData
}
};
45 changes: 27 additions & 18 deletions lib/rules/no-text-as-children.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
'use strict';

module.exports = function (context) {
var config = context.settings.i18n;
var minimatch = require('minimatch');

if (!config || config.ignoreFiles && new RegExp(config.ignoreFiles).test(context.getFilename())) {
return {};
}
module.exports = {
meta: {
docs: {
description: 'ensures that no plain text is used in JSX components',
category: 'Possible errors'
},
schema: []
},
create: function create(context) {
var config = context.settings.i18n;

return {
JSXElement: function JSXElement(node) {
node.children.forEach(function (child) {
if (child.type === 'Literal') {
var text = child.raw.trim().replace('\\n', '');
if (text.length) {
context.report({ node: child, message: 'Untranslated text \'' + text + '\'' });
}
}
});
if (config && config.ignoreFiles && minimatch(context.getFilename(), config.ignoreFiles)) {
return {};
}
};
};

module.exports.schema = [];
return {
JSXElement: function JSXElement(node) {
node.children.forEach(function (child) {
if (child.type === 'Literal') {
var text = child.raw.trim().replace('\\n', '');
if (text.length) {
context.report({ node: child, message: 'Untranslated text \'' + text + '\'' });
}
}
});
}
};
}
};
121 changes: 63 additions & 58 deletions lib/rules/no-unknown-key.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,81 +2,86 @@

var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();

var _appRootPath = require('app-root-path');
var minimatch = require('minimatch');

var _appRootPath2 = _interopRequireDefault(_appRootPath);

var _utils = require('../utils/utils');

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var _require = require('../utils/utils'),
getKeyValue = _require.getKeyValue,
has = _require.has,
getLangConfig = _require.getLangConfig;

module.exports = function (langsKey) {
return function (context) {
var langConfig = [];
var config = context.settings.i18n;
return {
meta: {
docs: {
description: 'ensures that used translate key is in translation file',
category: 'Possible errors'
},
schema: []
},
create: function create(context) {
var config = context.settings.i18n;

if (!config || config.ignoreFiles && minimatch(context.getFilename(), config.ignoreFiles)) {
return {};
}

if (!config || config.ignoreFiles && new RegExp(config.ignoreFiles).test(context.getFilename())) {
return {};
}
return {
CallExpression: function CallExpression(node) {
var funcName = node.callee.type === 'MemberExpression' && node.callee.property.name || node.callee.name;

if (config && config[langsKey]) {
langConfig = config[langsKey].map(function (_ref) {
var name = _ref.name,
translationPath = _ref.translationPath;
return {
name: name,
translation: require(_appRootPath2.default + '/' + translationPath)
};
});
}
if (funcName !== config.functionName || !node.arguments || !node.arguments.length) {
return;
}

return {
CallExpression: function CallExpression(node) {
var funcName = node.callee.type === 'MemberExpression' && node.callee.property.name || node.callee.name;
var _node$arguments = _slicedToArray(node.arguments, 3),
keyNode = _node$arguments[0],
countNode = _node$arguments[2];

if (funcName !== config.functionName || !node.arguments || !node.arguments.length) {
return;
}
var key = getKeyValue(keyNode);

var _node$arguments = _slicedToArray(node.arguments, 2),
keyNode = _node$arguments[0],
countNode = _node$arguments[1];
if (!key) {
return;
}

var key = (0, _utils.getKeyValue)(keyNode);
getLangConfig(config, langsKey).forEach(function (_ref) {
var name = _ref.name,
translation = _ref.translation;

if (!key) {
return;
}

if (typeof countNode === 'undefined') {
langConfig.forEach(function (_ref2) {
var name = _ref2.name,
translation = _ref2.translation;
if (!translation) {
context.report({
node: node,
severity: 2,
message: '\'' + name + '\' language is missing'
});

if (!(0, _utils.has)(translation, key)) {
context.report({ node: node, severity: 2, message: '\'' + key + '\' is missing from \'' + name + '\' language' });
return;
}
});
} else if (config.pluralizedKeys && config.pluralizedKeys.length) {
langConfig.forEach(function (_ref3) {
var name = _ref3.name,
translation = _ref3.translation;

var missingKeys = config.pluralizedKeys.reduce(function (accumulator, plural) {
return (0, _utils.has)(translation, key + '.' + plural) ? accumulator : accumulator.concat(plural);
}, []);

if (missingKeys.length) {
if (typeof countNode === 'undefined' && !has(translation, key)) {
context.report({
node: node,
message: '[' + missingKeys + '] keys are missing for key \'' + key + '\' in \'' + name + '\' language'
severity: 2,
message: '\'' + key + '\' is missing from \'' + name + '\' language'
});

return;
}

if (countNode && Array.isArray(config.pluralizedKeys)) {
var missingKeys = config.pluralizedKeys.filter(function (plural) {
return !has(translation, key + '.' + plural);
});

if (missingKeys.length) {
context.report({
node: node,
message: '[' + missingKeys + '] keys are missing for key \'' + key + '\' in \'' + name + '\' language'
});
}
}
});
}
}
};
};
}
};
};

module.exports.schema = [];
};
51 changes: 42 additions & 9 deletions lib/utils/utils.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,59 @@
'use strict';

Object.defineProperty(exports, "__esModule", {
value: true
});
var _has = function _has(object, keys, index) {
var fs = require('fs');
var path = require('path');
var appRootPath = require('app-root-path');

var recursiveGet = function recursiveGet(object, keys, index) {
if (keys.length - index === 1) {
return !!object[keys[index]];
return object[keys[index]];
}

return object[keys[index]] ? _has(object[keys[index]], keys, index + 1) : false;
return object[keys[index]] ? recursiveGet(object[keys[index]], keys, index + 1) : undefined;
};

exports.has = function (object, key) {
return !!recursiveGet(object, key.split('.'), 0);
};

var has = exports.has = function has(object, key) {
return _has(object, key.split('.'), 0);
exports.get = function (object, key) {
return recursiveGet(object, key.split('.'), 0);
};

var getKeyValue = exports.getKeyValue = function getKeyValue(key) {
exports.getKeyValue = function (key) {
if (key.type === 'Literal') {
return key.value;
} else if (key.type === 'TemplateLiteral' && key.quasis.length === 1) {
return key.quasis[0].value.cooked;
}

return null;
};

var langConfig = void 0;
var expireAt = 0;
exports.getLangConfig = function (config, languagesKey) {
if (expireAt <= Date.now() || config.disableCache) {
langConfig = config[languagesKey].map(function (_ref) {
var name = _ref.name,
translationPath = _ref.translationPath;

try {
var langFile = JSON.parse(fs.readFileSync(path.resolve(appRootPath + '/' + translationPath)).toString());

return {
name: name,
translation: langFile
};
} catch (e) {
return {
name: name,
translation: null
};
}
});
expireAt = Date.now() + (config.translationsCacheTTL || 500);
}

return langConfig;
};
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"build": "babel src --out-dir lib",
"babel-watch": "babel src --out-dir lib --watch",
"format": "prettier-eslint --write src/**/*.js",
"lint": "eslint src/**/*.js"
"lint": "eslint src/**/*.js",
"test": "mocha --recursive tests",
"prepack": "yarn lint && yarn test && yarn build"
},
"peerDependencies": {
"eslint": "^3.19.0"
Expand All @@ -29,11 +31,14 @@
"eslint-plugin-jsx-a11y": "^5.0.1",
"eslint-plugin-prettier": "^2.0.1",
"eslint-plugin-react": "^7.0.1",
"mocha": "^3.4.2",
"mversion": "^1.10.1",
"prettier": "^1.3.0",
"prettier-eslint-cli": "4.0.0"
"prettier-eslint-cli": "^4.0.0",
"sinon": "^2.3.7"
},
"dependencies": {
"app-root-path": "2.0.1"
"app-root-path": "2.0.1",
"minimatch": "3.0.4"
}
}
Loading

0 comments on commit d088dc6

Please sign in to comment.