[I18n] Support interpreting individual i18n-values as html or text-only (#26274) (#26469)

* [I18n] Add attribute for interpreting i18n-values as html or text-only

* Switch over to html_ prefixed values solution

* Update readme
This commit is contained in:
Leanid Shutau 2018-12-03 10:11:49 +03:00 committed by GitHub
parent 01dc301d6f
commit f1f5f1c9b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 209 additions and 91 deletions

View file

@ -378,19 +378,33 @@ The translation `directive` has the following syntax:
```html
<ANY
i18n-id="{string}"
i18n-default-message="{string}"
[i18n-values="{object}"]
[i18n-default-message="{string}"]
[i18n-description="{string}"]
></ANY>
```
Where:
- `i18n-id` - translation id to be translated
- `i18n-values` - values to pass into translation
- `i18n-default-message` - will be used unless translation was successful
- `i18n-values` - values to pass into translation
- `i18n-description` - optional context comment that will be extracted by i18n tools
and added as a comment next to translation message at `defaultMessages.json`
If HTML rendering in `i18n-values` is required then value key in `i18n-values` object
should have `html_` prefix. Otherwise the value will be inserted to the message without
HTML rendering.\
Example:
```html
<p
i18n-id="namespace.id"
i18n-default-message="Text with an emphasized {text}."
i18n-values="{
html_text: '<em>text</em>',
}"
></p>
```
Angular `I18n` module is placed into `autoload` module, so it will be
loaded automatically. After that we can use i18n directive in Angular templates:
```html

View file

@ -1,5 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`i18nDirective doesn't render html in result message with text-only values 1`] = `
<div
class="ng-scope ng-isolate-scope"
i18n-default-message="Default {one} onclick=alert(1) {two} message"
i18n-id="id"
i18n-values="{ one: '<span', two: '>Press</span>' }"
>
Default &lt;span onclick=alert(1) &gt;Press&lt;/span&gt; message
</div>
`;
exports[`i18nDirective doesn't render html in text-only value 1`] = `
<div
class="ng-scope ng-isolate-scope"
i18n-default-message="Default {value}"
i18n-id="id"
i18n-values="{ value: '<strong>message</strong>' }"
>
Default &lt;strong&gt;message&lt;/strong&gt;
</div>
`;
exports[`i18nDirective inserts correct translation html content with values 1`] = `"default-message word"`;
exports[`i18nDirective inserts correct translation html content with values 2`] = `"default-message anotherWord"`;
@ -9,7 +31,7 @@ exports[`i18nDirective sanitizes message before inserting it to DOM 1`] = `
class="ng-scope ng-isolate-scope"
i18n-default-message="Default message, {value}"
i18n-id="id"
i18n-values="{ value: '<div ng-click=\\"dangerousAction()\\"></div>' }"
i18n-values="{ html_value: '<div ng-click=\\"dangerousAction()\\"></div>' }"
>
Default message,
<div />
@ -19,9 +41,9 @@ exports[`i18nDirective sanitizes message before inserting it to DOM 1`] = `
exports[`i18nDirective sanitizes onclick attribute 1`] = `
<div
class="ng-scope ng-isolate-scope"
i18n-default-message="Default {one} onclick=alert(1) {two} message"
i18n-default-message="Default {value} message"
i18n-id="id"
i18n-values="{ one: '<span', two: '>Press</span>' }"
i18n-values="{ html_value: '<span onclick=alert(1)>Press</span>' }"
>
Default
<span>
@ -36,7 +58,7 @@ exports[`i18nDirective sanitizes onmouseover attribute 1`] = `
class="ng-scope ng-isolate-scope"
i18n-default-message="Default {value} message"
i18n-id="id"
i18n-values="{ value: '<span onmouseover=\\"alert(1)\\">Press</span>' }"
i18n-values="{ html_value: '<span onmouseover=\\"alert(1)\\">Press</span>' }"
>
Default
<span>

View file

@ -89,7 +89,7 @@ describe('i18nDirective', () => {
`<div
i18n-id="id"
i18n-default-message="Default message, {value}"
i18n-values="{ value: '<div ng-click=&quot;dangerousAction()&quot;></div>' }"
i18n-values="{ html_value: '<div ng-click=&quot;dangerousAction()&quot;></div>' }"
/>`
);
@ -99,7 +99,7 @@ describe('i18nDirective', () => {
expect(element[0]).toMatchSnapshot();
});
test('sanitizes onclick attribute', () => {
test(`doesn't render html in result message with text-only values`, () => {
const element = angular.element(
`<div
i18n-id="id"
@ -114,12 +114,42 @@ describe('i18nDirective', () => {
expect(element[0]).toMatchSnapshot();
});
test('sanitizes onclick attribute', () => {
const element = angular.element(
`<div
i18n-id="id"
i18n-default-message="Default {value} message"
i18n-values="{ html_value: '<span onclick=alert(1)>Press</span>' }"
/>`
);
compile(element)(scope);
scope.$digest();
expect(element[0]).toMatchSnapshot();
});
test('sanitizes onmouseover attribute', () => {
const element = angular.element(
`<div
i18n-id="id"
i18n-default-message="Default {value} message"
i18n-values="{ value: '<span onmouseover=&quot;alert(1)&quot;>Press</span>' }"
i18n-values="{ html_value: '<span onmouseover=&quot;alert(1)&quot;>Press</span>' }"
/>`
);
compile(element)(scope);
scope.$digest();
expect(element[0]).toMatchSnapshot();
});
test(`doesn't render html in text-only value`, () => {
const element = angular.element(
`<div
i18n-id="id"
i18n-default-message="Default {value}"
i18n-values="{ value: '<strong>message</strong>' }"
/>`
);

View file

@ -27,6 +27,9 @@ interface I18nScope extends IScope {
id: string;
}
const HTML_KEY_PREFIX = 'html_';
const PLACEHOLDER_SEPARATOR = '@I18N@';
export function i18nDirective(
i18n: I18nServiceType,
$sanitize: (html: string) => string
@ -41,27 +44,66 @@ export function i18nDirective(
link($scope, $element) {
if ($scope.values) {
$scope.$watchCollection('values', () => {
setHtmlContent($element, $scope, $sanitize, i18n);
setContent($element, $scope, $sanitize, i18n);
});
} else {
setHtmlContent($element, $scope, $sanitize, i18n);
setContent($element, $scope, $sanitize, i18n);
}
},
};
}
function setHtmlContent(
function setContent(
$element: IRootElementService,
$scope: I18nScope,
$sanitize: (html: string) => string,
i18n: I18nServiceType
) {
$element.html(
$sanitize(
i18n($scope.id, {
values: $scope.values,
defaultMessage: $scope.defaultMessage,
})
)
);
const originalValues = $scope.values;
const valuesWithPlaceholders = {} as Record<string, any>;
let hasValuesWithPlaceholders = false;
// If we have values with the keys that start with HTML_KEY_PREFIX we should replace
// them with special placeholders that later on will be inserted as HTML
// into the DOM, the rest of the content will be treated as text. We don't
// sanitize values at this stage as some of the values can be excluded from
// the translated string (e.g. not used by ICU conditional statements).
if (originalValues) {
for (const [key, value] of Object.entries(originalValues)) {
if (key.startsWith(HTML_KEY_PREFIX)) {
valuesWithPlaceholders[
key.slice(HTML_KEY_PREFIX.length)
] = `${PLACEHOLDER_SEPARATOR}${key}${PLACEHOLDER_SEPARATOR}`;
hasValuesWithPlaceholders = true;
} else {
valuesWithPlaceholders[key] = value;
}
}
}
const label = i18n($scope.id, {
values: valuesWithPlaceholders,
defaultMessage: $scope.defaultMessage,
});
// If there are no placeholders to replace treat everything as text, otherwise
// insert label piece by piece replacing every placeholder with corresponding
// sanitized HTML content.
if (!hasValuesWithPlaceholders) {
$element.text(label);
} else {
$element.empty();
for (const contentOrPlaceholder of label.split(PLACEHOLDER_SEPARATOR)) {
if (!contentOrPlaceholder) {
continue;
}
$element.append(
originalValues!.hasOwnProperty(contentOrPlaceholder)
? $sanitize(originalValues![contentOrPlaceholder])
: document.createTextNode(contentOrPlaceholder)
);
}
}
}

View file

@ -29,7 +29,7 @@
i18n-id="console.welcomePage.supportedRequestFormatDescription"
i18n-default-message="While typing a request, Console will make suggestions which you can then accept by hitting Enter/Tab.
These suggestions are made based on the request structure {asWellAs} your indices and types."
i18n-values="{ asWellAs: '<i>' + asWellAsFragmentText + '</i>' }"
i18n-values="{ html_asWellAs: '<i>' + asWellAsFragmentText + '</i>' }"
>
</p>

View file

@ -205,10 +205,10 @@
<div class="hintbox" ng-show="!editorState.params.gauge.colorsRange.length">
<p>
<i class="fa fa-danger text-danger"></i>
<span
<span
i18n-id="kbnVislibVisTypes.controls.gaugeOptions.specifiedRangeNumberWarningMessage"
i18n-default-message="{required} You must specify at least one range."
i18n-values="{ required: '<strong>' + requiredText + '</strong>' }"
i18n-values="{ html_required: '<strong>' + requiredText + '</strong>' }"
></span>
</p>
</div>

View file

@ -195,8 +195,8 @@
i18n-id="kbnVislibVisTypes.controls.heatmapOptions.specifiedRangeNumberWarningMessage"
i18n-default-message="{icon} {required} You must specify at least one range."
i18n-values="{
icon: '<span class=\'kuiIcon fa-danger text-danger\'></span>',
required: '<strong>' + requiredText + '</strong>'
html_icon: '<span class=\'kuiIcon fa-danger text-danger\'></span>',
html_required: '<strong>' + requiredText + '</strong>'
}"
></p>
</div>

View file

@ -77,8 +77,8 @@
i18n-id="kbn.dashboard.addVisualizationDescription2"
i18n-default-message=" button in the menu bar above to add a visualization to the dashboard. {br}If you haven't set up any visualizations yet, {visitVisualizeAppLink} to create your first visualization."
i18n-values="{
br: '<br/>',
visitVisualizeAppLink: '<a class=\'kuiLink\' href=\'#/visualize\'>' + visitVisualizeAppLinkText + '</a>'
html_br: '<br/>',
html_visitVisualizeAppLink: '<a class=\'kuiLink\' href=\'#/visualize\'>' + visitVisualizeAppLinkText + '</a>'
}"
></span>
</p>

View file

@ -33,7 +33,7 @@
<p class="kuiText kuiVerticalRhythm">
<span i18n-id="kbn.management.editIndexPattern.timeFilterLabel.timeFilterDetail"
i18n-default-message="This page lists every field in the {indexPatternTitle} index and the field's associated core type as recorded by Elasticsearch. To change a field type, use the Elasticsearch"
i18n-values="{ indexPatternTitle: '<strong>' + indexPattern.title + '</strong>' }"></span>
i18n-values="{ html_indexPatternTitle: '<strong>' + indexPattern.title + '</strong>' }"></span>
<a target="_window" class="euiLink euiLink--primary" href="http://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html">
<span i18n-id="kbn.management.editIndexPattern.timeFilterLabel.mappingAPILink"
i18n-default-message="Mapping API"></span>

View file

@ -113,7 +113,7 @@
<span
i18n-id="metricVis.params.ranges.warning.specifyRangeDescription"
i18n-default-message="{requiredDescription} You must specify at least one range."
i18n-values="{ requiredDescription: '<strong>' + editorState.requiredDescription + '</strong>' }"
i18n-values="{ html_requiredDescription: '<strong>' + editorState.requiredDescription + '</strong>' }"
>
</span>
</p>

View file

@ -57,7 +57,7 @@
i18n-id="tileMap.wmsOptions.wmsDescription"
i18n-default-message="WMS is an OGC standard for map image services. For more information, go {wmsLink}."
i18n-values="{
wmsLink: '<a href=\'http://www.opengeospatial.org/standards/wms\'>' + wmsLinkText + '</a>'
html_wmsLink: '<a href=\'http://www.opengeospatial.org/standards/wms\'>' + wmsLinkText + '</a>'
}"
></p>
<br>

View file

@ -4,7 +4,7 @@
<h1
i18n-id="timelion.help.welcomeTitle"
i18n-default-message="Welcome to {strongTimelionLabel}!"
i18n-values="{ strongTimelionLabel: '<strong>Timelion</strong>' }"
i18n-values="{ html_strongTimelionLabel: '<strong>Timelion</strong>' }"
></h1>
<p
i18n-id="timelion.help.welcome.content.paragraph1"
@ -16,14 +16,14 @@
easy-to-master expression syntax. This tutorial focuses on
Elasticsearch, but you'll quickly discover that what you learn here
applies to any datasource Timelion supports."
i18n-values="{ emphasizedEverything: '<em>' + translations.emphasizedEverythingText + '</em>' }"
i18n-values="{ html_emphasizedEverything: '<em>' + translations.emphasizedEverythingText + '</em>' }"
></p>
<p>
<span
i18n-id="timelion.help.welcome.content.paragraph2"
i18n-default-message="Ready to get started? Click {strongNext}. Want to skip the tutorial and view the docs?"
i18n-values="{
strongNext: '<strong>' + translations.strongNextText + '</strong>',
html_strongNext: '<strong>' + translations.strongNextText + '</strong>',
}"
></span>
<a
@ -66,9 +66,9 @@
indices, go to {advancedSettingsPath} and configure the {esDefaultIndex}
and {esTimefield} settings to match your indices."
i18n-values="{
advancedSettingsPath: '<strong>' + translations.notValidAdvancedSettingsPath + '</strong>',
esDefaultIndex: '<code>timelion:es.default_index</code>',
esTimefield: '<code>timelion:es.timefield</code>',
html_advancedSettingsPath: '<strong>' + translations.notValidAdvancedSettingsPath + '</strong>',
html_esDefaultIndex: '<code>timelion:es.default_index</code>',
html_esTimefield: '<code>timelion:es.timefield</code>',
}"
></p>
<p
@ -93,7 +93,7 @@
i18n-default-message="Could not validate Elasticsearch settings: {reason}.
Check your Advanced Settings and try again. ({count})"
i18n-values="{
reason: '<strong>' + es.invalidReason + '</strong>',
html_reason: '<strong>' + es.invalidReason + '</strong>',
count: es.invalidCount,
}"
></span>
@ -120,8 +120,8 @@
looks ok. We found data from {statsMin} to {statsMax}.
You're probably all set. If this doesn't look right, see"
i18n-values="{
statsMin: '<strong>' + es.stats.min + '</strong>',
statsMax: '<strong>' + es.stats.max + '</strong>',
html_statsMin: '<strong>' + es.stats.min + '</strong>',
html_statsMax: '<strong>' + es.stats.max + '</strong>',
}"
i18n-description="Part of composite text timelion.help.configuration.valid.paragraph1Part1 +
timelion.help.configuration.firstTimeConfigurationLinkText +
@ -159,7 +159,7 @@
i18n-id="timelion.help.configuration.valid.intervalsTextPart1"
i18n-default-message="The interval selector at the right of the input bar lets you
control the sampling frequency. It's currently set to {interval}."
i18n-values="{ interval: '<code>' + state.interval + '</code>' }"
i18n-values="{ html_interval: '<code>' + state.interval + '</code>' }"
i18n-description="Part of composite text
timelion.help.configuration.valid.intervalsTextPart1 +
(timelion.help.configuration.valid.intervalIsAutoText ||
@ -186,7 +186,7 @@
(timelion.help.configuration.valid.intervalIsAutoText ||
timelion.help.configuration.valid.intervals.content.intervalIsNotAutoText) +
timelion.help.configuration.valid.intervalsTextPart2"
i18n-values="{ auto: '<code>auto </code>' }"
i18n-values="{ html_auto: '<code>auto</code>' }"
></span>
<span
i18n-id="timelion.help.configuration.valid.intervalsTextPart2"
@ -194,8 +194,8 @@
will produce too many data points, it throws an error.
You can adjust that limit by configuring {maxBuckets} in {advancedSettingsPath}."
i18n-values="{
maxBuckets: '<code>timelion:max_buckets</code>',
advancedSettingsPath: '<strong>' + translations.validAdvancedSettingsPath + '</strong>',
html_maxBuckets: '<code>timelion:max_buckets</code>',
html_advancedSettingsPath: '<strong>' + translations.validAdvancedSettingsPath + '</strong>',
}"
></span>
</p>
@ -250,7 +250,7 @@
datasource, you can start submitting queries. For starters,
enter {esPattern} in the input bar and hit enter."
i18n-values="{
esPattern: '<code>.es(*)</code>',
html_esPattern: '<code>.es(*)</code>',
}"
></p>
<p>
@ -262,13 +262,13 @@
field that is greater than 100. Note that this query is enclosed in single
quotes&mdash;that's because it contains spaces. You can enter any"
i18n-values="{
esAsteriskQueryDescription: '<em>' + translations.esAsteriskQueryDescription + '</em>',
html: '<em>html</em>',
htmlQuery: '<code>.es(html)</code>',
bobQuery: '<code>.es(\'user:bob AND bytes:>100\')</code>',
bob: '<em>bob</em>',
user: '<code>user</code>',
bytes: '<code>bytes</code>',
html_esAsteriskQueryDescription: '<em>' + translations.esAsteriskQueryDescription + '</em>',
html_html: '<em>html</em>',
html_htmlQuery: '<code>.es(html)</code>',
html_bobQuery: '<code>.es(\'user:bob AND bytes:>100\')</code>',
html_bob: '<em>bob</em>',
html_user: '<code>user</code>',
html_bytes: '<code>bytes</code>',
}"
i18n-description="Part of composite text
timelion.help.querying.paragraph2Part1 +
@ -290,7 +290,7 @@
i18n-id="timelion.help.querying.paragraph2Part2"
i18n-default-message="as the first argument to the {esQuery} function."
i18n-values="{
esQuery: '<code>.es()</code>',
html_esQuery: '<code>.es()</code>',
}"
i18n-description="Part of composite text
timelion.help.querying.paragraph2Part1 +
@ -312,10 +312,10 @@
For example, you can enter {esLogstashQuery} to tell the Elasticsearch datasource
{esIndexQueryDescription}."
i18n-values="{
esEmptyQuery: '<code>.es()</code>',
esStarQuery: '<code>.es(*)</code>',
esLogstashQuery: '<code>.es(index=\'logstash-*\', q=\'*\')</code>',
esIndexQueryDescription: '<em>' + translations.esIndexQueryDescription + '</em>',
html_esEmptyQuery: '<code>.es()</code>',
html_esStarQuery: '<code>.es(*)</code>',
html_esLogstashQuery: '<code>.es(index=\'logstash-*\', q=\'*\')</code>',
html_esIndexQueryDescription: '<em>' + translations.esIndexQueryDescription + '</em>',
}"
></p>
<h4
@ -350,15 +350,15 @@
Simply use the {cardinality} metric: {esCardinalityQuery}. To get the
average of the {bytes} field, you can use the {avg} metric: {esAvgQuery}."
i18n-values="{
min: '<code>min</code>',
max: '<code>max</code>',
avg: '<code>avg</code>',
sum: '<code>sum</code>',
cardinality: '<code>cardinality</code>',
bytes: '<code>bytes</code>',
srcIp: '<code>src_ip</code>',
esCardinalityQuery: '<code>.es(*, metric=\'cardinality:src_ip\')</code>',
esAvgQuery: '<code>.es(metric=\'avg:bytes\')</code>',
html_min: '<code>min</code>',
html_max: '<code>max</code>',
html_avg: '<code>avg</code>',
html_sum: '<code>sum</code>',
html_cardinality: '<code>cardinality</code>',
html_bytes: '<code>bytes</code>',
html_srcIp: '<code>src_ip</code>',
html_esCardinalityQuery: '<code>.es(*, metric=\'cardinality:src_ip\')</code>',
html_esAvgQuery: '<code>.es(metric=\'avg:bytes\')</code>',
}"
i18n-description="Part of composite text
timelion.help.querying.countTextPart1 +
@ -410,7 +410,7 @@
to add another chart or three. Then, select a chart,
copy one of the following expressions, paste it into the input bar,
and hit enter. Rinse, repeat to try out the other expressions."
i18n-values="{ strongAdd: '<strong>' + translations.strongAddText + '</strong>' }"
i18n-values="{ html_strongAdd: '<strong>' + translations.strongAddText + '</strong>' }"
></p>
<table class="table table-condensed table-striped">
<tr>
@ -419,7 +419,7 @@
i18n-id="timelion.help.expressions.examples.twoExpressionsDescription"
i18n-default-message="{descriptionTitle} Two expressions on the same chart."
i18n-values="{
descriptionTitle: '<strong>' + translations.twoExpressionsDescriptionTitle + '</strong>',
html_descriptionTitle: '<strong>' + translations.twoExpressionsDescriptionTitle + '</strong>',
}"
></td>
</tr>
@ -430,7 +430,7 @@
i18n-default-message="{descriptionTitle} Colorizes the first series red and
uses 1 pixel wide bars for the second series."
i18n-values="{
descriptionTitle: '<strong>' + translations.customStylingDescriptionTitle + '</strong>',
html_descriptionTitle: '<strong>' + translations.customStylingDescriptionTitle + '</strong>',
}"
></td>
</tr>
@ -445,7 +445,7 @@
to specify arguments in, use named arguments to make
the expressions easier to read and write."
i18n-values="{
descriptionTitle: '<strong>' + translations.namedArgumentsDescriptionTitle + '</strong>',
html_descriptionTitle: '<strong>' + translations.namedArgumentsDescriptionTitle + '</strong>',
}"
></td>
</tr>
@ -456,7 +456,7 @@
i18n-default-message="{descriptionTitle} You can also chain groups of expressions to
functions. Here, both series are shown as points instead of lines."
i18n-values="{
descriptionTitle: '<strong>' + translations.groupedExpressionsDescriptionTitle + '</strong>',
html_descriptionTitle: '<strong>' + translations.groupedExpressionsDescriptionTitle + '</strong>',
}"
></td>
</tr>
@ -515,7 +515,7 @@
<p
i18n-id="timelion.help.dataTransforming.paragraph2"
i18n-default-message="First, we need to find all events that contain US: {esUsQuery}."
i18n-values="{ esUsQuery: '<code>.es(\'US\')</code>' }"
i18n-values="{ html_esUsQuery: '<code>.es(\'US\')</code>' }"
></p>
<p
i18n-id="timelion.help.dataTransforming.paragraph3"
@ -523,16 +523,16 @@
To divide {us} by everything, we can use the {divide} function:
{divideDataQuery}."
i18n-values="{
us: '<code>\'US\'</code>',
divide: '<code>divide</code>',
divideDataQuery: '<code>.es(\'US\').divide(.es())</code>',
html_us: '<code>\'US\'</code>',
html_divide: '<code>divide</code>',
html_divideDataQuery: '<code>.es(\'US\').divide(.es())</code>',
}"
></p>
<p
i18n-id="timelion.help.dataTransforming.paragraph4"
i18n-default-message="Not bad, but this gives us a number between 0 and 1. To convert it
to a percentage, simply multiply by 100: {multiplyDataQuery}."
i18n-values="{ multiplyDataQuery: '<code>.es(\'US\').divide(.es()).multiply(100)</code>' }"
i18n-values="{ html_multiplyDataQuery: '<code>.es(\'US\').divide(.es()).multiply(100)</code>' }"
></p>
<p
i18n-id="timelion.help.dataTransforming.paragraph5"
@ -543,13 +543,13 @@
also other useful data transformation functions, such as
{movingaverage}, {abs}, and {derivative}."
i18n-values="{
sum: '<code>sum</code>',
subtract: '<code>subtract</code>',
multiply: '<code>multiply</code>',
divide: '<code>divide</code>',
movingaverage: '<code>movingaverage</code>',
abs: '<code>abs</code>',
derivative: '<code>derivative</code>',
html_sum: '<code>sum</code>',
html_subtract: '<code>subtract</code>',
html_multiply: '<code>multiply</code>',
html_divide: '<code>divide</code>',
html_movingaverage: '<code>movingaverage</code>',
html_abs: '<code>abs</code>',
html_derivative: '<code>derivative</code>',
}"
></p>
<p>

View file

@ -40,6 +40,7 @@ const ESCAPE_LINE_BREAK_REGEX = /(?<!\\)\\\n/g;
const HTML_LINE_BREAK_REGEX = /[\s]*\n[\s]*/g;
const ARGUMENT_ELEMENT_TYPE = 'argumentElement';
const HTML_KEY_PREFIX = 'html_';
export const readFileAsync = promisify(fs.readFile);
export const writeFileAsync = promisify(fs.writeFile);
@ -162,17 +163,21 @@ function extractValueReferencesFromIcuAst(node, keys = new Set()) {
/**
* Checks whether values from "values" and "defaultMessage" correspond to each other.
*
* @param {string[]} valuesKeys array of "values" property keys
* @param {string[]} prefixedValuesKeys array of "values" property keys
* @param {string} defaultMessage "defaultMessage" value
* @param {string} messageId message id for fail errors
* @throws if "values" and "defaultMessage" don't correspond to each other
*/
export function checkValuesProperty(valuesKeys, defaultMessage, messageId) {
export function checkValuesProperty(prefixedValuesKeys, defaultMessage, messageId) {
// skip validation if defaultMessage doesn't use ICU and values prop has no keys
if (!valuesKeys.length && !defaultMessage.includes('{')) {
if (!prefixedValuesKeys.length && !defaultMessage.includes('{')) {
return;
}
const valuesKeys = prefixedValuesKeys.map(
key => (key.startsWith(HTML_KEY_PREFIX) ? key.slice(HTML_KEY_PREFIX.length) : key)
);
let defaultMessageAst;
try {

View file

@ -625,7 +625,7 @@
class="col-sm-10"
i18n-id="xpack.graph.sidebar.similarLabels.keyTermsText"
i18n-default-message="Key terms: {inferredEdgeLabel}"
i18n-values="{ inferredEdgeLabel: '<small>' + detail.inferredEdge.label + '</small>' }"
i18n-values="{ html_inferredEdgeLabel: '<small>' + detail.inferredEdge.label + '</small>' }"
></div>
</div>
</div>

View file

@ -23,21 +23,26 @@
class="kuiTitle kuiVerticalRhythmSmall"
i18n-id="xpack.searchProfiler.licenseErrorMessageTitle"
i18n-default-message="{warningIcon} License error"
i18n-values="{ warningIcon: '<span class=\'kuiIcon fa-warning kuiIcon--error\'></span>' }"
i18n-values="{ html_warningIcon: '<span class=\'kuiIcon fa-warning kuiIcon--error\'></span>' }"
></h2>
<p
class="kuiText kuiVerticalRhythmSmall"
i18n-id="xpack.searchProfiler.licenseErrorMessageDescription"
i18n-default-message="The Profiler Visualization requires an active license ({licenseTypeList} or {platinumLicenseType}), but none were found in your cluster."
i18n-values="{ licenseTypeList: '<code>' + trialLicense + '</code>, <code>' + basicLicense + '</code>, <code>' + goldLicense + '</code>', platinumLicenseType: '<code>' + platinumLicense + '</code>' }"
i18n-values="{
html_licenseTypeList: '<code>' + trialLicense + '</code>, <code>' + basicLicense + '</code>, <code>' + goldLicense + '</code>',
html_platinumLicenseType: '<code>' + platinumLicense + '</code>',
}"
></p>
<p
class="kuiText kuiVerticalRhythmSmall"
i18n-id="xpack.searchProfiler.registerLicenseDescription"
i18n-default-message="Please {registerLicenseLink} to continue using the Search Profiler"
i18n-values="{ registerLicenseLink: '<a class=\'kuiLink\' href=\'https://www.elastic.co/subscriptions\' rel=\'noopener noreferrer\'>' + registerLicenseLinkLabel + '</a>' }"
i18n-values="{
html_registerLicenseLink: '<a class=\'kuiLink\' href=\'https://www.elastic.co/subscriptions\' rel=\'noopener noreferrer\'>' + registerLicenseLinkLabel + '</a>',
}"
></p>
</div>
</div>

View file

@ -142,7 +142,7 @@
i18n-id="xpack.security.management.roles.reversedTitle"
i18n-default-message="Reserved {icon}"
i18n-values="{
icon: '<span
html_icon: '<span
class=\'kuiIcon fa-question-circle\'
tooltip={{reversedTooltip}}
aria-label={{reversedAriaLabel}}
@ -187,7 +187,7 @@
i18n-id="xpack.security.management.roles.disableTitle"
i18n-default-message="{icon} Disabled"
i18n-values="{
icon: '<span class=\'kuiIcon fa-warning\'></span>'
html_icon: '<span class=\'kuiIcon fa-warning\'></span>'
}"
>
<span class="kuiIcon fa-warning"></span>