[I18n] Allow i18n filter usage outside of interpolation expressions (#26803)
* [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:
parent
df593e3e57
commit
9aecea4dd2
|
@ -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
|
||||
[37m[41m [49m[39m 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 [37m[41ma[49m[39ms ('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\\")."`;
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -79,9 +79,9 @@
|
|||
<input id="displayWarnings" type="checkbox" ng-model="editorState.params.isDisplayWarning">
|
||||
|
||||
<icon-tip
|
||||
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."'
|
||||
} }}"
|
||||
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">
|
||||
|
||||
<icon-tip
|
||||
content="{{::'regionMap.visParams.turnOffShowingAllShapesTipText' | i18n: {
|
||||
defaultMessage: '"Turning this off only shows the shapes that were matched with a corresponding term"'
|
||||
} }}"
|
||||
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>
|
||||
|
|
|
@ -80,7 +80,9 @@
|
|||
>
|
||||
|
||||
<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>
|
||||
|
|
|
@ -45,7 +45,9 @@
|
|||
>
|
||||
|
||||
<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"
|
||||
|
|
Loading…
Reference in a new issue