Skip to content
22 changes: 16 additions & 6 deletions src/dev/i18n/extractors/__snapshots__/html.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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\\")."`;
76 changes: 25 additions & 51 deletions src/dev/i18n/extractors/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);

if (i18nMarkerPosition === -1) {
return;
}

const linesCount = (stringWithoutExpressions.slice(0, i18nMarkerPosition).match(/\n/g) || [])
.length;
function* extractExpressions(htmlContent) {
const elements = cheerio
.load(htmlContent)('*')
.toArray();

const errorWithContext = createParserErrorMessage(string, {
loc: {
line: linesCount + 1,
column: 0,
},
message: 'I18n filter can be used only in interpolation expressions',
});
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);
}
}

throw createFailError(errorWithContext);
for (const attribute of Object.values(element.attribs)) {
if (attribute.includes(I18N_FILTER_MARKER)) {
yield trimCurlyBraces(attribute);
}
}
}
}

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);
Expand Down Expand Up @@ -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'),
Expand Down
15 changes: 14 additions & 1 deletion src/dev/i18n/extractors/html.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Comment thread
LeanidShutau marked this conversation as resolved.
const source = Buffer.from(`\
<div
ng-options="mode as ('metricVis.colorModes.' + mode | i18n: { defaultMessage: mode }) for mode in collections.metricColorMode"
Expand All @@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
46 changes: 39 additions & 7 deletions src/legacy/core_plugins/tile_map/public/editors/wms_options.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand Down