[Tools] Forbid i18n filter usage outside of interpolation expressions (#23982) (#24644)

* [I18n] Forbid i18n filter usage outside of interpolation expressions

* Add tests

* Add usage examples to JSDoc
This commit is contained in:
Leanid Shutau 2018-10-29 13:53:09 +03:00 committed by GitHub
parent 9487c012f6
commit 94637b538f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 61 additions and 3 deletions

View file

@ -40,4 +40,12 @@ Array [
exports[`dev/i18n/extractors/html throws on empty i18n-id 1`] = `"Empty \\"i18n-id\\" value in angular directive is not allowed."`;
exports[`dev/i18n/extractors/html throws on i18n filter usage in angular directive argument 1`] = `
"I18n filter can be used only in interpolation expressions:
<div
  ng-options=\\"mode as ('metricVis.colorModes.' + mode | i18n: { defaultMessage: mode }) for mode in collections.metricColorMode\\"
></div>
"
`;
exports[`dev/i18n/extractors/html throws on missing i18n-default-message attribute 1`] = `"Empty defaultMessage in angular directive is not allowed (\\"message-id\\")."`;

View file

@ -36,9 +36,11 @@ import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY, VALUES_KEY } from '../constants';
import { createFailError } from '../../run';
/**
* Find all substrings of "{{ any text }}" pattern
* Find all substrings of "{{ any text }}" pattern allowing '{' and '}' chars in single quote strings
*
* Example: `{{ ::'message.id' | i18n: { defaultMessage: 'Message with {{curlyBraces}}' } }}`
*/
const ANGULAR_EXPRESSION_REGEX = /\{\{+([\s\S]*?)\}\}+/g;
const ANGULAR_EXPRESSION_REGEX = /{{([^{}]|({([^']|('([^']|(\\'))*'))*?}))*}}+/g;
const I18N_FILTER_MARKER = '| i18n: ';
@ -113,7 +115,12 @@ function parseIdExpression(expression) {
const stringNode = [...traverseNodes(ast.program.directives)].find(node =>
isDirectiveLiteral(node)
);
return stringNode ? formatJSString(stringNode.value) : null;
if (!stringNode) {
throw createFailError(`Message id should be a string literal, but got: \n${expression}`);
}
return formatJSString(stringNode.value);
}
function trimCurlyBraces(string) {
@ -148,7 +155,40 @@ function trimOneTimeBindingOperator(string) {
return string;
}
/**
* Remove interpolation expressions from angular and throw on `| i18n:` substring.
*
* Correct usage: `<p aria-label="{{ ::'namespace.id' | i18n: { defaultMessage: 'Message' } }}"></p>`.
*
* Incorrect usage: `ng-options="mode as ('metricVis.colorModes.' + mode | i18n: { defaultMessage: mode }) for mode in collections.metricColorMode"`
*
* @param {string} string html content
*/
function validateI18nFilterUsage(string) {
const stringWithoutExpressions = string.replace(ANGULAR_EXPRESSION_REGEX, '');
const i18nMarkerPosition = stringWithoutExpressions.indexOf(I18N_FILTER_MARKER);
if (i18nMarkerPosition === -1) {
return;
}
const linesCount = (stringWithoutExpressions.slice(0, i18nMarkerPosition).match(/\n/g) || [])
.length;
const errorWithContext = createParserErrorMessage(string, {
loc: {
line: linesCount + 1,
column: 0,
},
message: 'I18n filter can be used only in interpolation expressions',
});
throw createFailError(errorWithContext);
}
function* getFilterMessages(htmlContent) {
validateI18nFilterUsage(htmlContent);
const expressions = (htmlContent.match(ANGULAR_EXPRESSION_REGEX) || [])
.filter(expression => expression.includes(I18N_FILTER_MARKER))
.map(trimCurlyBraces);

View file

@ -71,6 +71,16 @@ describe('dev/i18n/extractors/html', () => {
<p
i18n-id="message-id"
></p>
`);
expect(() => extractHtmlMessages(source).next()).toThrowErrorMatchingSnapshot();
});
test('throws on i18n filter usage in angular directive argument', () => {
const source = Buffer.from(`\
<div
ng-options="mode as ('metricVis.colorModes.' + mode | i18n: { defaultMessage: mode }) for mode in collections.metricColorMode"
></div>
`);
expect(() => extractHtmlMessages(source).next()).toThrowErrorMatchingSnapshot();