[I18n] Support interpreting individual i18n-values as html or text-only (#26274)
* [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:
parent
b3a618c585
commit
c800f8d592
|
@ -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
|
||||
|
|
|
@ -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 <span onclick=alert(1) >Press</span> 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 <strong>message</strong>
|
||||
</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>
|
||||
|
|
|
@ -89,7 +89,7 @@ describe('i18nDirective', () => {
|
|||
`<div
|
||||
i18n-id="id"
|
||||
i18n-default-message="Default message, {value}"
|
||||
i18n-values="{ value: '<div ng-click="dangerousAction()"></div>' }"
|
||||
i18n-values="{ html_value: '<div ng-click="dangerousAction()"></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="alert(1)">Press</span>' }"
|
||||
i18n-values="{ html_value: '<span onmouseover="alert(1)">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>' }"
|
||||
/>`
|
||||
);
|
||||
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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—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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue