Skip to content

Commit

Permalink
[Tools] Forbid i18n filter usage outside of interpolation expressions (
Browse files Browse the repository at this point in the history
…#23982)

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

* Add tests

* Add usage examples to JSDoc
  • Loading branch information
LeanidShutau authored Oct 26, 2018
1 parent afbc9d7 commit a392385
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 3 deletions.
8 changes: 8 additions & 0 deletions src/dev/i18n/extractors/__snapshots__/html.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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\\")."`;
46 changes: 43 additions & 3 deletions src/dev/i18n/extractors/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: ';

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions src/dev/i18n/extractors/html.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit a392385

Please sign in to comment.