[I18n] Allow i18n filter usage outside of interpolation expressions (#26803) (#27032)

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

* Remove redundant quotes from translation

* Update tests

* Resolve comments

* Fix wrong filter usage
This commit is contained in:
Leanid Shutau 2018-12-12 17:59:15 +03:00 committed by GitHub
parent 9d0d7894e8
commit 024dc31602
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 103 additions and 72 deletions

View file

@ -38,14 +38,24 @@ Array [
]
`;
exports[`dev/i18n/extractors/html extracts message from i18n filter in interpolating directive 1`] = `
Array [
Array [
"namespace.messageId",
Object {
"description": undefined,
"message": "Message",
},
],
]
`;
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 i18n filter usage in complex angular expression 1`] = `
"Couldn't parse angular i18n expression:
Unexpected token, expected \\";\\" (1:6):
mode as ('metricVis.colorModes.' + mode"
`;
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

@ -109,18 +109,8 @@ function parseIdExpression(expression) {
}
function trimCurlyBraces(string) {
return string.slice(2, -2).trim();
}
/**
* Removes parentheses from the start and the end of a string.
*
* Example: `('id' | i18n: { defaultMessage: 'Message' })`
* @param {string} string string to trim
*/
function trimParentheses(string) {
if (string.startsWith('(') && string.endsWith(')')) {
return string.slice(1, -1);
if (string.startsWith('{{') && string.endsWith('}}')) {
return string.slice(2, -2).trim();
}
return string;
@ -140,54 +130,37 @@ 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);
function* extractExpressions(htmlContent) {
const elements = cheerio
.load(htmlContent)('*')
.toArray();
if (i18nMarkerPosition === -1) {
return;
for (const element of elements) {
for (const node of element.children) {
if (node.type === 'text') {
yield* (node.data.match(ANGULAR_EXPRESSION_REGEX) || [])
.filter(expression => expression.includes(I18N_FILTER_MARKER))
.map(trimCurlyBraces);
}
}
for (const attribute of Object.values(element.attribs)) {
if (attribute.includes(I18N_FILTER_MARKER)) {
yield trimCurlyBraces(attribute);
}
}
}
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);
for (const expression of expressions) {
for (const expression of extractExpressions(htmlContent)) {
const filterStart = expression.indexOf(I18N_FILTER_MARKER);
const idExpression = trimParentheses(
trimOneTimeBindingOperator(expression.slice(0, filterStart).trim())
);
const idExpression = trimOneTimeBindingOperator(expression.slice(0, filterStart).trim());
const filterObjectExpression = expression.slice(filterStart + I18N_FILTER_MARKER.length).trim();
if (!filterObjectExpression || !idExpression) {
throw createFailError(`Cannot parse i18n filter expression: {{ ${expression} }}`);
throw createFailError(`Cannot parse i18n filter expression: ${expression}`);
}
const messageId = parseIdExpression(idExpression);
@ -217,8 +190,9 @@ function* getDirectiveMessages(htmlContent) {
const $ = cheerio.load(htmlContent);
const elements = $('[i18n-id]')
.map(function (idx, el) {
.map((idx, el) => {
const $el = $(el);
return {
id: $el.attr('i18n-id'),
defaultMessage: $el.attr('i18n-default-message'),

View file

@ -80,7 +80,7 @@ describe('dev/i18n/extractors/html', () => {
expect(() => extractHtmlMessages(source).next()).toThrowErrorMatchingSnapshot();
});
test('throws on i18n filter usage in angular directive argument', () => {
test('throws on i18n filter usage in complex angular expression', () => {
const source = Buffer.from(`\
<div
ng-options="mode as ('metricVis.colorModes.' + mode | i18n: { defaultMessage: mode }) for mode in collections.metricColorMode"
@ -89,4 +89,17 @@ describe('dev/i18n/extractors/html', () => {
expect(() => extractHtmlMessages(source).next()).toThrowErrorMatchingSnapshot();
});
test('extracts message from i18n filter in interpolating directive', () => {
const source = Buffer.from(`
<icon-tip
content="::'namespace.messageId' | i18n: {
defaultMessage: 'Message'
}"
position="'right'"
></icon-tip>
`);
expect(Array.from(extractHtmlMessages(source))).toMatchSnapshot();
});
});

View file

@ -79,9 +79,9 @@
<input id="displayWarnings" type="checkbox" ng-model="editorState.params.isDisplayWarning">
&nbsp;
<icon-tip
content="{{::'regionMap.visParams.switchWarningsTipText' | i18n: {
defaultMessage: '&quot;Turns on/off warnings. When turned on, warning will be shown for each term that cannot be matched to a shape in the vector layer based on the join field. When turned off, these warnings will be turned off.&quot;'
} }}"
content="::'regionMap.visParams.switchWarningsTipText' | i18n: {
defaultMessage: 'Turns on/off warnings. When turned on, warning will be shown for each term that cannot be matched to a shape in the vector layer based on the join field. When turned off, these warnings will be turned off.'
}"
position="'right'"
></icon-tip>
</div>
@ -97,9 +97,9 @@
<input id="onlyShowMatchingShapes" type="checkbox" ng-model="editorState.params.showAllShapes">
&nbsp;
<icon-tip
content="{{::'regionMap.visParams.turnOffShowingAllShapesTipText' | i18n: {
defaultMessage: '&quot;Turning this off only shows the shapes that were matched with a corresponding term&quot;'
} }}"
content="::'regionMap.visParams.turnOffShowingAllShapesTipText' | i18n: {
defaultMessage: 'Turning this off only shows the shapes that were matched with a corresponding term'
}"
position="'right'"
></icon-tip>
</div>

View file

@ -80,7 +80,9 @@
>
&nbsp;
<icon-tip
content="{{::'tileMap.visParams.reduceVibrancyOfTileColorsTip' | i18n: {defaultMessage: '\'Reduce the vibrancy of tile colors. This does not work in any version of Internet Explorer.\''} }}"
content="::'tileMap.visParams.reduceVibrancyOfTileColorsTip' | i18n: {
defaultMessage: 'Reduce the vibrancy of tile colors. This does not work in any version of Internet Explorer.'
}"
position="'right'"
></icon-tip>
</div>

View file

@ -45,7 +45,9 @@
>
&nbsp;
<icon-tip
content="{{::'tileMap.wmsOptions.useWMSCompliantMapTileServerTip' | i18n: {defaultMessage: '\'Use WMS compliant map tile server. For advanced users only.\''} }}"
content="::'tileMap.wmsOptions.useWMSCompliantMapTileServerTip' | i18n: {
defaultMessage: 'Use WMS compliant map tile server. For advanced users only.'
}"
position="'right'"
></icon-tip>
</div>
@ -66,7 +68,12 @@
i18n-id="tileMap.wmsOptions.wmsUrlLabel"
i18n-default-message="WMS url*"
></span>
<icon-tip position="'right'" content="{{::'tileMap.wmsOptions.urlOfWMSWebServiceTip' | i18n: {defaultMessage: '\'The URL of the WMS web service\''} }}"></icon-tip>
<icon-tip
content="::'tileMap.wmsOptions.urlOfWMSWebServiceTip' | i18n: {
defaultMessage: 'The URL of the WMS web service'
}"
position="'right'"
></icon-tip>
</label>
<input type="text" class="form-control"
name="wms.url"
@ -79,7 +86,12 @@
i18n-id="tileMap.wmsOptions.wmsLayersLabel"
i18n-default-message="WMS layers*"
></span>
<icon-tip position="'right'" content="{{::'tileMap.wmsOptions.listOfLayersToUseTip' | i18n: {defaultMessage: '\'A comma separated list of layers to use\''} }}"></icon-tip>
<icon-tip
content="::'tileMap.wmsOptions.listOfLayersToUseTip' | i18n: {
defaultMessage: 'A comma separated list of layers to use'
}"
position="'right'"
></icon-tip>
</label>
<input type="text" class="form-control"
ng-require="options.enabled"
@ -93,7 +105,12 @@
i18n-id="tileMap.wmsOptions.wmsVersionLabel"
i18n-default-message="WMS version*"
></span>
<icon-tip position="'right'" content="{{::'tileMap.wmsOptions.versionOfWMSserverSupportsTip' | i18n: {defaultMessage: '\'The version of WMS the server supports\''} }}"></icon-tip>
<icon-tip
content="::'tileMap.wmsOptions.versionOfWMSserverSupportsTip' | i18n: {
defaultMessage: 'The version of WMS the server supports'
}"
position="'right'"
></icon-tip>
</label>
<input type="text" class="form-control"
name="wms.options.version"
@ -106,7 +123,12 @@
i18n-id="tileMap.wmsOptions.wmsFormatLabel"
i18n-default-message="WMS format*"
></span>
<icon-tip position="'right'" content="{{::'tileMap.wmsOptions.imageFormatToUseTip' | i18n: {defaultMessage: '\'Usually image/png or image/jpeg. Use png if the server will return transparent layers.\''} }}"></icon-tip>
<icon-tip
content="::'tileMap.wmsOptions.imageFormatToUseTip' | i18n: {
defaultMessage: 'Usually image/png or image/jpeg. Use png if the server will return transparent layers.'
}"
position="'right'"
></icon-tip>
</label>
<input type="text" class="form-control"
name="wms.options.format"
@ -119,7 +141,12 @@
i18n-id="tileMap.wmsOptions.wmsAttributionLabel"
i18n-default-message="WMS attribution"
></span>
<icon-tip position="'right'" content="{{::'tileMap.wmsOptions.attributionStringTip' | i18n: {defaultMessage: '\'Attribution string for the lower right corner\''} }}"></icon-tip>
<icon-tip
content="::'tileMap.wmsOptions.attributionStringTip' | i18n: {
defaultMessage: 'Attribution string for the lower right corner'
}"
position="'right'"
></icon-tip>
</label>
<input type="text" class="form-control"
name="wms.options.attribution"
@ -132,7 +159,12 @@
i18n-id="tileMap.wmsOptions.wmsStylesLabel"
i18n-default-message="WMS styles*"
></span>
<icon-tip position="'right'" content="{{::'tileMap.wmsOptions.wmsServerSupportedStylesListTip' | i18n: {defaultMessage: '\'A comma separated list of WMS server supported styles to use. Blank in most cases.\''} }}"></icon-tip>
<icon-tip
content="::'tileMap.wmsOptions.wmsServerSupportedStylesListTip' | i18n: {
defaultMessage: 'A comma separated list of WMS server supported styles to use. Blank in most cases.'
}"
position="'right'"
></icon-tip>
</label>
<input type="text" class="form-control"
name="wms.options.styles"