diff --git a/src/dev/i18n/extractors/__snapshots__/html.test.js.snap b/src/dev/i18n/extractors/__snapshots__/html.test.js.snap index 222a5429eda95..bf9b882752376 100644 --- a/src/dev/i18n/extractors/__snapshots__/html.test.js.snap +++ b/src/dev/i18n/extractors/__snapshots__/html.test.js.snap @@ -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: +
+" +`; + exports[`dev/i18n/extractors/html throws on missing i18n-default-message attribute 1`] = `"Empty defaultMessage in angular directive is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/extractors/html.js b/src/dev/i18n/extractors/html.js index b9ae2bb04b451..e8090fc136edf 100644 --- a/src/dev/i18n/extractors/html.js +++ b/src/dev/i18n/extractors/html.js @@ -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: `

`. + * + * 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); diff --git a/src/dev/i18n/extractors/html.test.js b/src/dev/i18n/extractors/html.test.js index 3b0133f229c56..609b052fa0e38 100644 --- a/src/dev/i18n/extractors/html.test.js +++ b/src/dev/i18n/extractors/html.test.js @@ -71,6 +71,16 @@ describe('dev/i18n/extractors/html', () => {

+`); + + expect(() => extractHtmlMessages(source).next()).toThrowErrorMatchingSnapshot(); + }); + + test('throws on i18n filter usage in angular directive argument', () => { + const source = Buffer.from(`\ +
`); expect(() => extractHtmlMessages(source).next()).toThrowErrorMatchingSnapshot();