[I18n] Add one-time binding to angularjs i18n (#23499)
* Add one-time binding to angularjs i18n * Add watcher for values property * Watch values field only if it is provided * Fix ci
This commit is contained in:
parent
4246530213
commit
14e4e1744c
14 changed files with 116 additions and 42 deletions
|
@ -18,7 +18,7 @@ The following types are supported:
|
|||
- ToggleSwitch
|
||||
- LinkLabel and etc.
|
||||
|
||||
There is one more complex case, when we have to divide a single expression into different labels.
|
||||
There is one more complex case, when we have to divide a single expression into different labels.
|
||||
|
||||
For example the message before translation looks like:
|
||||
|
||||
|
@ -221,8 +221,8 @@ For example:
|
|||
|
||||
```js
|
||||
<button
|
||||
aria-label="{{'kbn.management.editIndexPattern.removeAriaLabel' | i18n: {defaultMessage: 'Remove index pattern'} }}"
|
||||
tooltip="{{'kbn.management.editIndexPattern.removeTooltip' | i18n: {defaultMessage: 'Remove index pattern'} }}"
|
||||
aria-label="{{ ::'kbn.management.editIndexPattern.removeAriaLabel' | i18n: {defaultMessage: 'Remove index pattern'} }}"
|
||||
tooltip="{{ ::'kbn.management.editIndexPattern.removeTooltip' | i18n: {defaultMessage: 'Remove index pattern'} }}"
|
||||
>
|
||||
</button>
|
||||
```
|
||||
|
@ -333,4 +333,3 @@ it('should render normally', async () => {
|
|||
});
|
||||
// ...
|
||||
```
|
||||
|
||||
|
|
|
@ -188,7 +188,7 @@ import { i18n } from '@kbn/i18n';
|
|||
i18n.init(messages);
|
||||
```
|
||||
|
||||
One common use-case is that of internationalizing a string constant. Here's an
|
||||
One common use-case is that of internationalizing a string constant. Here's an
|
||||
example of how we'd do that:
|
||||
|
||||
```js
|
||||
|
@ -399,7 +399,7 @@ In order to translate attributes in Angular we should use `i18nFilter`:
|
|||
```html
|
||||
<input
|
||||
type="text"
|
||||
placeholder="{{'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER' | i18n: {
|
||||
placeholder="{{ ::'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER' | i18n: {
|
||||
defaultMessage: 'Search'
|
||||
} }}"
|
||||
>
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
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"`;
|
|
@ -30,7 +30,7 @@ angular
|
|||
|
||||
describe('i18nDirective', () => {
|
||||
let compile: angular.ICompileService;
|
||||
let scope: angular.IRootScopeService;
|
||||
let scope: angular.IRootScopeService & { word?: string };
|
||||
|
||||
beforeEach(angular.mock.module('app'));
|
||||
beforeEach(
|
||||
|
@ -38,6 +38,7 @@ describe('i18nDirective', () => {
|
|||
($compile: angular.ICompileService, $rootScope: angular.IRootScopeService) => {
|
||||
compile = $compile;
|
||||
scope = $rootScope.$new();
|
||||
scope.word = 'word';
|
||||
}
|
||||
)
|
||||
);
|
||||
|
@ -62,19 +63,23 @@ describe('i18nDirective', () => {
|
|||
test('inserts correct translation html content with values', () => {
|
||||
const id = 'id';
|
||||
const defaultMessage = 'default-message {word}';
|
||||
const compiledContent = 'default-message word';
|
||||
|
||||
const element = angular.element(
|
||||
`<div
|
||||
i18n-id="${id}"
|
||||
i18n-default-message="${defaultMessage}"
|
||||
i18n-values="{ word: 'word' }"
|
||||
i18n-values="{ word }"
|
||||
/>`
|
||||
);
|
||||
|
||||
compile(element)(scope);
|
||||
scope.$digest();
|
||||
|
||||
expect(element.html()).toEqual(compiledContent);
|
||||
expect(element.html()).toMatchSnapshot();
|
||||
|
||||
scope.word = 'anotherWord';
|
||||
scope.$digest();
|
||||
|
||||
expect(element.html()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,26 +21,37 @@ import { IDirective, IRootElementService, IScope } from 'angular';
|
|||
|
||||
import { I18nServiceType } from './provider';
|
||||
|
||||
export function i18nDirective(i18n: I18nServiceType): IDirective {
|
||||
interface I18nScope extends IScope {
|
||||
values?: Record<string, any>;
|
||||
defaultMessage: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export function i18nDirective(i18n: I18nServiceType): IDirective<I18nScope> {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
id: '@i18nId',
|
||||
defaultMessage: '@i18nDefaultMessage',
|
||||
values: '=i18nValues',
|
||||
values: '<?i18nValues',
|
||||
},
|
||||
link($scope: IScope, $element: IRootElementService) {
|
||||
$scope.$watchGroup(
|
||||
['id', 'defaultMessage', 'values'],
|
||||
([id, defaultMessage = '', values = {}]) => {
|
||||
$element.html(
|
||||
i18n(id, {
|
||||
values,
|
||||
defaultMessage,
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
link($scope, $element) {
|
||||
if ($scope.values) {
|
||||
$scope.$watchCollection('values', () => {
|
||||
setHtmlContent($element, $scope, i18n);
|
||||
});
|
||||
} else {
|
||||
setHtmlContent($element, $scope, i18n);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setHtmlContent($element: IRootElementService, $scope: I18nScope, i18n: I18nServiceType) {
|
||||
$element.html(
|
||||
i18n($scope.id, {
|
||||
values: $scope.values,
|
||||
defaultMessage: $scope.defaultMessage,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
data-test-subj="editIndexPattern"
|
||||
class="kuiViewContent"
|
||||
role="region"
|
||||
aria-label="{{'kbn.management.editIndexPattern.detailsAria' | i18n: { defaultMessage: 'Index pattern details' } }}"
|
||||
aria-label="{{::'kbn.management.editIndexPattern.detailsAria' | i18n: { defaultMessage: 'Index pattern details' } }}"
|
||||
>
|
||||
<!-- Header -->
|
||||
<kbn-management-index-header
|
||||
|
@ -103,9 +103,9 @@
|
|||
<input
|
||||
class="kuiSearchInput__input"
|
||||
type="text"
|
||||
aria-label="{{'kbn.management.editIndexPattern.fields.filterAria' | i18n: {defaultMessage: 'Filter'} }}"
|
||||
aria-label="{{::'kbn.management.editIndexPattern.fields.filterAria' | i18n: {defaultMessage: 'Filter'} }}"
|
||||
ng-model="fieldFilter"
|
||||
placeholder="{{'kbn.management.editIndexPattern.fields.filterPlaceholder' | i18n: {defaultMessage: 'Filter'} }}"
|
||||
placeholder="{{::'kbn.management.editIndexPattern.fields.filterPlaceholder' | i18n: {defaultMessage: 'Filter'} }}"
|
||||
data-test-subj="indexPatternFieldFilter"
|
||||
>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<a
|
||||
data-test-subj="indexPatternFieldEditButton"
|
||||
ng-href="{{ kbnUrl.getRouteHref(field, 'edit') }}"
|
||||
aria-label="{{'kbn.management.editIndexPattern.editFieldButton' | i18n: { defaultMessage: 'Edit' } }}"
|
||||
aria-label="{{::'kbn.management.editIndexPattern.editFieldButton' | i18n: { defaultMessage: 'Edit' } }}"
|
||||
class="kuiButton kuiButton--basic kuiButton--small"
|
||||
>
|
||||
<span aria-hidden="true" class="kuiIcon fa-pencil"></span>
|
||||
|
@ -12,7 +12,7 @@
|
|||
ng-if="field.scripted"
|
||||
ng-click="remove(field)"
|
||||
class="kuiButton kuiButton--danger kuiButton--small"
|
||||
aria-label="{{'kbn.management.editIndexPattern.deleteFieldButton' | i18n: { defaultMessage: 'Delete' } }}"
|
||||
aria-label="{{::'kbn.management.editIndexPattern.deleteFieldButton' | i18n: { defaultMessage: 'Delete' } }}"
|
||||
>
|
||||
<span aria-hidden="true" class="kuiIcon fa-trash"></span>
|
||||
</button>
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
<button
|
||||
ng-if="setDefault"
|
||||
ng-click="setDefault()"
|
||||
aria-label="{{'kbn.management.editIndexPattern.setDefaultAria' | i18n: { defaultMessage: 'Set as default index' } }}"
|
||||
tooltip="{{'kbn.management.editIndexPattern.setDefaultTooltip' | i18n: { defaultMessage: 'Set as default index' } }}"
|
||||
aria-label="{{::'kbn.management.editIndexPattern.setDefaultAria' | i18n: { defaultMessage: 'Set as default index' } }}"
|
||||
tooltip="{{::'kbn.management.editIndexPattern.setDefaultTooltip' | i18n: { defaultMessage: 'Set as default index' } }}"
|
||||
class="kuiButton kuiButton--basic"
|
||||
data-test-subj="setDefaultIndexPatternButton"
|
||||
>
|
||||
|
@ -32,8 +32,8 @@
|
|||
<button
|
||||
ng-if="refreshFields"
|
||||
ng-click="refreshFields()"
|
||||
aria-label="{{'kbn.management.editIndexPattern.refreshAria' | i18n: { defaultMessage: 'Reload field list' } }}"
|
||||
tooltip="{{'kbn.management.editIndexPattern.refreshTooltip' | i18n: { defaultMessage: 'Refresh field list' } }}"
|
||||
aria-label="{{::'kbn.management.editIndexPattern.refreshAria' | i18n: { defaultMessage: 'Reload field list' } }}"
|
||||
tooltip="{{::'kbn.management.editIndexPattern.refreshTooltip' | i18n: { defaultMessage: 'Refresh field list' } }}"
|
||||
class="kuiButton kuiButton--basic"
|
||||
>
|
||||
<span
|
||||
|
@ -45,8 +45,8 @@
|
|||
<button
|
||||
ng-if="delete"
|
||||
ng-click="delete()"
|
||||
aria-label="{{'kbn.management.editIndexPattern.removeAria' | i18n: { defaultMessage: 'Remove index pattern' } }}"
|
||||
tooltip="{{'kbn.management.editIndexPattern.removeTooltip' | i18n: { defaultMessage: 'Remove index pattern' } }}"
|
||||
aria-label="{{::'kbn.management.editIndexPattern.removeAria' | i18n: { defaultMessage: 'Remove index pattern' } }}"
|
||||
tooltip="{{::'kbn.management.editIndexPattern.removeTooltip' | i18n: { defaultMessage: 'Remove index pattern' } }}"
|
||||
class="kuiButton kuiButton--danger"
|
||||
data-test-subj="deleteIndexPatternButton"
|
||||
>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="euiPage">
|
||||
<div class="col-md-2 sidebar-container" role="region" aria-label="{{'kbn.management.editIndexPatternAria' | i18n: { defaultMessage: 'Index patterns' } }}">
|
||||
<div class="col-md-2 sidebar-container" role="region" aria-label="{{::'kbn.management.editIndexPatternAria' | i18n: { defaultMessage: 'Index patterns' } }}">
|
||||
<div class="sidebar-list">
|
||||
<div class="sidebar-item-title full-title">
|
||||
<h5 data-test-subj="createIndexPatternParent">
|
||||
|
@ -55,4 +55,4 @@
|
|||
<div ng-transclude></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
kbn-accessible-click
|
||||
aria-expanded="{{!!showColorRange}}"
|
||||
aria-controls="metricOptionsRanges"
|
||||
aria-label="{{'metricVis.params.ranges.toggleOptionsAriaLabel' | i18n: { defaultMessage: 'Toggle range options' } }}"
|
||||
aria-label="{{::'metricVis.params.ranges.toggleOptionsAriaLabel' | i18n: { defaultMessage: 'Toggle range options' } }}"
|
||||
class="kuiSideBarCollapsibleTitle__label"
|
||||
ng-click="showColorRange = !showColorRange"
|
||||
>
|
||||
|
@ -135,7 +135,7 @@
|
|||
kbn-accessible-click
|
||||
aria-expanded="{{!!showColorOptions}}"
|
||||
aria-controls="metricOptionsColors"
|
||||
aria-label="{{'metricVis.params.color.toggleOptionsAriaLabel' | i18n: { defaultMessage: 'Toggle color options' } }}"
|
||||
aria-label="{{::'metricVis.params.color.toggleOptionsAriaLabel' | i18n: { defaultMessage: 'Toggle color options' } }}"
|
||||
class="kuiSideBarCollapsibleTitle__label"
|
||||
ng-click="showColorOptions = !showColorOptions"
|
||||
>
|
||||
|
@ -219,7 +219,7 @@
|
|||
kbn-accessible-click
|
||||
aria-expanded="{{!!showStyle}}"
|
||||
aria-controls="metricOptionsStyle"
|
||||
aria-label="{{'metricVis.params.style.toggleOptionsAriaLabel' | i18n: { defaultMessage: 'Toggle style options' } }}"
|
||||
aria-label="{{::'metricVis.params.style.toggleOptionsAriaLabel' | i18n: { defaultMessage: 'Toggle style options' } }}"
|
||||
class="kuiSideBarCollapsibleTitle__label"
|
||||
ng-click="showStyle = !showStyle"
|
||||
>
|
||||
|
|
|
@ -1 +1 @@
|
|||
<p>{{ 'plugin_2.message-id' | i18n: { defaultMessage: 'Message text' } }}</p>
|
||||
<p>{{ ::'plugin_2.message-id' | i18n: { defaultMessage: 'Message text' } }}</p>
|
||||
|
|
|
@ -26,6 +26,18 @@ Array [
|
|||
]
|
||||
`;
|
||||
|
||||
exports[`dev/i18n/extractors/html extracts default messages from HTML with one-time binding 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
"kbn.id",
|
||||
Object {
|
||||
"context": undefined,
|
||||
"message": "Message text with {value}",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
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 missing i18n-default-message attribute 1`] = `"Empty defaultMessage in angular directive is not allowed (\\"message-id\\")."`;
|
||||
|
|
|
@ -119,6 +119,34 @@ 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);
|
||||
}
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes one-time binding operator `::` from the start of a string.
|
||||
*
|
||||
* Example: `::'id' | i18n: { defaultMessage: 'Message' }`
|
||||
* @param {string} string string to trim
|
||||
*/
|
||||
function trimOneTimeBindingOperator(string) {
|
||||
if (string.startsWith('::')) {
|
||||
return string.slice(2);
|
||||
}
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
function* getFilterMessages(htmlContent) {
|
||||
const expressions = (htmlContent.match(ANGULAR_EXPRESSION_REGEX) || [])
|
||||
.filter(expression => expression.includes(I18N_FILTER_MARKER))
|
||||
|
@ -126,7 +154,10 @@ function* getFilterMessages(htmlContent) {
|
|||
|
||||
for (const expression of expressions) {
|
||||
const filterStart = expression.indexOf(I18N_FILTER_MARKER);
|
||||
const idExpression = expression.slice(0, filterStart).trim();
|
||||
const idExpression = trimParentheses(
|
||||
trimOneTimeBindingOperator(expression.slice(0, filterStart).trim())
|
||||
);
|
||||
|
||||
const filterObjectExpression = expression.slice(filterStart + I18N_FILTER_MARKER.length).trim();
|
||||
|
||||
if (!filterObjectExpression || !idExpression) {
|
||||
|
|
|
@ -43,6 +43,17 @@ describe('dev/i18n/extractors/html', () => {
|
|||
expect(actual.sort()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('extracts default messages from HTML with one-time binding', () => {
|
||||
const actual = Array.from(
|
||||
extractHtmlMessages(`
|
||||
<div>
|
||||
{{::'kbn.id' | i18n: { defaultMessage: 'Message text with {value}', values: { value: 'value' } }}}
|
||||
</div>
|
||||
`)
|
||||
);
|
||||
expect(actual.sort()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('throws on empty i18n-id', () => {
|
||||
const source = Buffer.from(`\
|
||||
<p
|
||||
|
|
Loading…
Reference in a new issue