[i18n] remove angular i18n and move the remains to monitoring plugin (#115003)

This commit is contained in:
Ahmad Bamieh 2021-10-15 12:09:19 +03:00 committed by GitHub
parent c11b38de7b
commit d19510535a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 23 additions and 538 deletions

View file

@ -135,27 +135,6 @@ export const Component = () => {
Full details are {kib-repo}tree/master/packages/kbn-i18n#react[here].
[discrete]
==== i18n for Angular
You are encouraged to use `i18n.translate()` by statically importing `i18n` from `@kbn/i18n` wherever possible in your Angular code. Angular wrappers use the translation `service` with the i18n engine under the hood.
The translation directive has the following syntax:
["source","js"]
-----------
<ANY
i18n-id="{string}"
i18n-default-message="{string}"
[i18n-values="{object}"]
[i18n-description="{string}"]
></ANY>
-----------
Full details are {kib-repo}tree/master/packages/kbn-i18n#angularjs[here].
[discrete]
=== Resources

View file

@ -489,7 +489,6 @@
"@testing-library/react-hooks": "^5.1.1",
"@testing-library/user-event": "^13.1.1",
"@types/angular": "^1.6.56",
"@types/angular-mocks": "^1.7.0",
"@types/apidoc": "^0.22.3",
"@types/archiver": "^5.1.0",
"@types/babel__core": "^7.1.16",
@ -644,7 +643,6 @@
"@yarnpkg/lockfile": "^1.1.0",
"abab": "^2.0.4",
"aggregate-error": "^3.1.0",
"angular-mocks": "^1.7.9",
"antlr4ts-cli": "^0.5.0-alpha.3",
"apidoc": "^0.29.0",
"apidoc-markdown": "^6.0.0",

View file

@ -27,7 +27,6 @@ filegroup(
)
NPM_MODULE_EXTRA_FILES = [
"angular/package.json",
"react/package.json",
"package.json",
"GUIDELINE.md",
@ -47,7 +46,6 @@ TYPES_DEPS = [
"//packages/kbn-babel-preset",
"@npm//intl-messageformat",
"@npm//tslib",
"@npm//@types/angular",
"@npm//@types/intl-relativeformat",
"@npm//@types/jest",
"@npm//@types/prop-types",

View file

@ -93,17 +93,6 @@ The long term plan is to rely on using `FormattedMessage` and `i18n.translate()`
Currently, we support the following ReactJS `i18n` tools, but they will be removed in future releases:
- Usage of `props.intl.formatmessage()` (where `intl` is passed to `props` by `injectI18n` HOC).
#### In AngularJS
The long term plan is to rely on using `i18n.translate()` by statically importing `i18n` from the `@kbn/i18n` package. **Avoid using the `i18n` filter and the `i18n` service injected in controllers, directives, services.**
- Call JS function `i18n.translate()` from the `@kbn/i18n` package.
- Use `i18nId` directive in template.
Currently, we support the following AngluarJS `i18n` tools, but they will be removed in future releases:
- Usage of `i18n` service in controllers, directives, services by injecting it.
- Usage of `i18n` filter in template for attribute translation. Note: Use one-time binding ("{{:: ... }}") in filters wherever it's possible to prevent unnecessary expression re-evaluation.
#### In JavaScript
- Use `i18n.translate()` in NodeJS or any other framework agnostic code, where `i18n` is the I18n engine from `@kbn/i18n` package.
@ -223,7 +212,6 @@ For example:
- for button:
```js
<EuiButton data-test-subj="addScriptedFieldLink" href={addScriptedFieldUrl}>
<FormattedMessage id="kbn.management.editIndexPattern.scripted.addFieldButtonLabel" defaultMessage="Add scripted field"/>
</EuiButton>
@ -232,11 +220,11 @@ For example:
- for dropDown:
```js
<select ng-model="indexedFieldTypeFilter" ng-options="o for o in indexedFieldTypes">
<option value=""
i18n-id="kbn.management.editIndexPattern.fields.allTypesDropDown"
i18n-default-message="All field types"></option>
</select>
<option value={
i18n.translate('kbn.management.editIndexPattern.fields.allTypesDropDown', {
defaultMessage: 'All field types',
})
}
```
- for placeholder:
@ -309,12 +297,6 @@ For example:
- Variables
```html
<span i18n-id="kbn.management.editIndexPattern.timeFilterHeader"
i18n-default-message="Time Filter field name: {timeFieldName}"
i18n-values="{ timeFieldName: indexPattern.timeFieldName }"></span>
```
```html
<FormattedMessage
id="kbn.management.createIndexPatternHeader"
@ -327,25 +309,6 @@ For example:
- Labels and variables in tag
```html
<span i18n-id="kbn.management.editIndexPattern.timeFilterLabel.timeFilterDetail"
i18n-default-message="This page lists every field in the {indexPatternTitle} index"
i18n-values="{ indexPatternTitle: '<strong>' + indexPattern.title + '</strong>' }"></span>
```
-----------------------------------------------------------
**BUT** we can not use tags that should be compiled:
```html
<span i18n-id="kbn.management.editIndexPattern.timeFilterLabel.timeFilterDetail"
i18n-default-message="This page lists every field in the {indexPatternTitle} index"
i18n-values="{ indexPatternTitle: '<div my-directive>' + indexPattern.title + '</div>' }"></span>
```
To void injections vulnerability, `i18nId` directive doesn't compile its values.
-----------------------------------------------------------
```html
<FormattedMessage
id="kbn.management.createIndexPattern.step.indexPattern.disallowLabel"
@ -388,10 +351,12 @@ The numeric input is mapped to a plural category, some subset of "zero", "one",
Here is an example of message translation depending on a plural category:
```html
<span i18n-id="kbn.management.editIndexPattern.mappingConflictLabel"
i18n-default-message="{conflictFieldsLength, plural, one {A field is} other {# fields are}} defined as several types (string, integer, etc) across the indices that match this pattern."
i18n-values="{ conflictFieldsLength: conflictFields.length }"></span>
```jsx
<FormattedMessage
id="kbn.management.editIndexPattern.mappingConflictLabel"
defaultMessage="{conflictFieldsLength, plural, one {A field is} other {# fields are}} defined as several types (string, integer, etc) across the indices that match this pattern."
values={{ conflictFieldsLength: conflictFields.length }}
/>
```
When `conflictFieldsLength` equals 1, the result string will be `"A field is defined as several types (string, integer, etc) across the indices that match this pattern."`. In cases when `conflictFieldsLength` has value of 2 or more, the result string - `"2 fields are defined as several types (string, integer, etc) across the indices that match this pattern."`.

View file

@ -343,98 +343,6 @@ export const MyComponent = injectI18n(
);
```
## AngularJS
The long term plan is to rely on using `i18n.translate()` by statically importing `i18n` from the `@kbn/i18n` package. **Avoid using the `i18n` filter and the `i18n` service injected in controllers, directives, services.**
AngularJS wrapper has 4 entities: translation `provider`, `service`, `directive`
and `filter`. Both the directive and the filter use the translation `service`
with i18n engine under the hood.
The translation `provider` is used for `service` configuration and
has the following methods:
- `addMessages(messages: Map<string, string>, [locale: string])` - provides a way to register
translations with the library
- `setLocale(locale: string)` - tells the library which language to use by given
language key
- `getLocale()` - returns the current locale
- `setDefaultLocale(locale: string)` - tells the library which language to fallback
when missing translations
- `getDefaultLocale()` - returns the default locale
- `setFormats(formats: object)` - supplies a set of options to the underlying formatter
- `getFormats()` - returns current formats
- `getRegisteredLocales()` - returns array of locales having translations
- `init(messages: Map<string, string>)` - initializes the engine
The translation `service` provides only one method:
- `i18n(id: string, { values: object, defaultMessage: string, description: string })`
translate message by id
The translation `filter` is used for attributes translation and has
the following syntax:
```
{{ ::'translationId' | i18n: { values: object, defaultMessage: string, description: string } }}
```
Where:
- `translationId` - translation id to be translated
- `values` - values to pass into translation
- `defaultMessage` - will be used unless translation was successful (the final
fallback in english, will be used for generating `en.json`)
- `description` - optional context comment that will be extracted by i18n tools
and added as a comment next to translation message at `defaultMessages.json`
The translation `directive` has the following syntax:
```html
<ANY
i18n-id="{string}"
i18n-default-message="{string}"
[i18n-values="{object}"]
[i18n-description="{string}"]
></ANY>
```
Where:
- `i18n-id` - translation id to be translated
- `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
<span
i18n-id="welcome"
i18n-default-message="Hello!"
></span>
```
In order to translate attributes in AngularJS we should use `i18nFilter`:
```html
<input
type="text"
placeholder="{{ ::'kbn.management.objects.searchAriaLabel' | i18n: {
defaultMessage: 'Search { title } Object',
values: { title }
} }}"
>
```
## I18n tools
In order to simplify localization process, some additional tools were implemented:

View file

@ -1,5 +0,0 @@
{
"browser": "../target_web/angular",
"main": "../target_node/angular",
"types": "../target_types/angular/index.d.ts"
}

View file

@ -1,69 +0,0 @@
// 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"`;
exports[`i18nDirective sanitizes message before inserting it to DOM 1`] = `
<div
class="ng-scope ng-isolate-scope"
i18n-default-message="Default message, {value}"
i18n-id="id"
i18n-values="{ html_value: '<div ng-click=\\"dangerousAction()\\"></div>' }"
>
Default message,
<div />
</div>
`;
exports[`i18nDirective sanitizes onclick attribute 1`] = `
<div
class="ng-scope ng-isolate-scope"
i18n-default-message="Default {value} message"
i18n-id="id"
i18n-values="{ html_value: '<span onclick=alert(1)>Press</span>' }"
>
Default
<span>
Press
</span>
message
</div>
`;
exports[`i18nDirective sanitizes onmouseover attribute 1`] = `
<div
class="ng-scope ng-isolate-scope"
i18n-default-message="Default {value} message"
i18n-id="id"
i18n-values="{ html_value: '<span onmouseover=\\"alert(1)\\">Press</span>' }"
>
Default
<span>
Press
</span>
message
</div>
`;

View file

@ -1,150 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import angular from 'angular';
import 'angular-mocks';
import 'angular-sanitize';
import { i18nDirective } from './directive';
import { I18nProvider } from './provider';
angular
.module('app', ['ngSanitize'])
.provider('i18n', I18nProvider)
.directive('i18nId', i18nDirective);
describe('i18nDirective', () => {
let compile: angular.ICompileService;
let scope: angular.IRootScopeService & { word?: string };
beforeEach(angular.mock.module('app'));
beforeEach(
angular.mock.inject(
($compile: angular.ICompileService, $rootScope: angular.IRootScopeService) => {
compile = $compile;
scope = $rootScope.$new();
scope.word = 'word';
}
)
);
test('inserts correct translation html content', () => {
const id = 'id';
const defaultMessage = 'default-message';
const element = angular.element(
`<div
i18n-id="${id}"
i18n-default-message="${defaultMessage}"
/>`
);
compile(element)(scope);
scope.$digest();
expect(element.html()).toEqual(defaultMessage);
});
test('inserts correct translation html content with values', () => {
const id = 'id';
const defaultMessage = 'default-message {word}';
const element = angular.element(
`<div
i18n-id="${id}"
i18n-default-message="${defaultMessage}"
i18n-values="{ word }"
/>`
);
compile(element)(scope);
scope.$digest();
expect(element.html()).toMatchSnapshot();
scope.word = 'anotherWord';
scope.$digest();
expect(element.html()).toMatchSnapshot();
});
test('sanitizes message before inserting it to DOM', () => {
const element = angular.element(
`<div
i18n-id="id"
i18n-default-message="Default message, {value}"
i18n-values="{ html_value: '<div ng-click=&quot;dangerousAction()&quot;></div>' }"
/>`
);
compile(element)(scope);
scope.$digest();
expect(element[0]).toMatchSnapshot();
});
test(`doesn't render html in result message with text-only values`, () => {
const element = angular.element(
`<div
i18n-id="id"
i18n-default-message="Default {one} onclick=alert(1) {two} message"
i18n-values="{ one: '<span', two: '>Press</span>' }"
/>`
);
compile(element)(scope);
scope.$digest();
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="{ 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>' }"
/>`
);
compile(element)(scope);
scope.$digest();
expect(element[0]).toMatchSnapshot();
});
});

View file

@ -1,46 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
jest.mock('../core/i18n', () => ({
translate: jest.fn().mockImplementation(() => 'translation'),
}));
import angular from 'angular';
import 'angular-mocks';
import * as i18n from '../core/i18n';
import { i18nFilter as angularI18nFilter } from './filter';
import { I18nProvider, I18nServiceType } from './provider';
angular.module('app', []).provider('i18n', I18nProvider).filter('i18n', angularI18nFilter);
describe('i18nFilter', () => {
let filter: I18nServiceType;
beforeEach(angular.mock.module('app'));
beforeEach(
angular.mock.inject((i18nFilter) => {
filter = i18nFilter;
})
);
afterEach(() => {
jest.resetAllMocks();
});
test('provides wrapper around i18n engine', () => {
const id = 'id';
const defaultMessage = 'default-message';
const values = {};
const result = filter(id, { defaultMessage, values });
expect(result).toEqual('translation');
expect(i18n.translate).toHaveBeenCalledTimes(1);
expect(i18n.translate).toHaveBeenCalledWith(id, { defaultMessage, values });
});
});

View file

@ -1,47 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import angular from 'angular';
import 'angular-mocks';
import * as i18nCore from '../core/i18n';
import { I18nProvider, I18nServiceType } from './provider';
angular.module('app', []).provider('i18n', I18nProvider);
describe('i18nProvider', () => {
let provider: I18nProvider;
let service: I18nServiceType;
beforeEach(
angular.mock.module('app', [
'i18nProvider',
(i18n: I18nProvider) => {
provider = i18n;
},
])
);
beforeEach(
angular.mock.inject((i18n: I18nServiceType) => {
service = i18n;
})
);
test('provides wrapper around i18n engine', () => {
expect(service).toEqual(i18nCore.translate);
});
test('provides service wrapper around i18n engine', () => {
const serviceMethodNames = Object.keys(provider);
const pluginMethodNames = Object.keys(i18nCore);
expect([...serviceMethodNames, 'translate'].sort()).toEqual(
[...pluginMethodNames, '$get'].sort()
);
});
});

View file

@ -15,7 +15,6 @@ require('./flot_charts');
// stateful deps
export const KbnI18n = require('@kbn/i18n');
export const KbnI18nAngular = require('@kbn/i18n/angular');
export const KbnI18nReact = require('@kbn/i18n/react');
export const Angular = require('angular');
export const EmotionReact = require('@emotion/react');

View file

@ -32,7 +32,6 @@ exports.externals = {
*/
angular: '__kbnSharedDeps__.Angular',
'@kbn/i18n': '__kbnSharedDeps__.KbnI18n',
'@kbn/i18n/angular': '__kbnSharedDeps__.KbnI18nAngular',
'@kbn/i18n/react': '__kbnSharedDeps__.KbnI18nReact',
'@emotion/react': '__kbnSharedDeps__.EmotionReact',
jquery: '__kbnSharedDeps__.Jquery',

View file

@ -18,34 +18,6 @@ The `defaultMessage` must contain ICU references to all keys in the `values` and
The `description` is optional, `values` is optional too unless `defaultMessage` references to it.
* **Angular (.html)**
* **Filter**
```
{{ ::'pluginNamespace.messageId' | i18n: {
defaultMessage: 'Default message string literal, {key}',
values: { key: 'value' },
description: 'Message context or description'
} }}
```
* Don't break `| i18n: {` with line breaks, and don't skip whitespaces around `i18n:`.
* `::` operator is optional. Omit it if you need data binding for the `values`.
* **Directive**
```html
<p
i18n-id="pluginNamespace.messageId"
i18n-default-message="Default message string literal, {key}. {emphasizedText}"
i18n-values="{ key: value, html_emphasizedText: htmlString }"
i18n-description="Message context or description"
></p>
```
* `html_` prefixes will be removed from `i18n-values` keys before validation.
* **React (.jsx, .tsx)**
* **\<FormattedMessage\>**

View file

@ -64,7 +64,7 @@
"xpack.observability": "plugins/observability",
"xpack.banners": "plugins/banners"
},
"exclude": ["examples"],
"exclude": ["examples", "plugins/monitoring/public/angular/angular_i18n"],
"translations": [
"plugins/translations/translations/zh-CN.json",
"plugins/translations/translations/ja-JP.json"

View file

@ -1,9 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { IDirective, IRootElementService, IScope } from 'angular';

View file

@ -1,9 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { I18nServiceType } from './provider';

View file

@ -1,9 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { I18nProvider } from './provider';

View file

@ -1,12 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as i18n from '../core';
import { i18n } from '@kbn/i18n';
export type I18nServiceType = ReturnType<I18nProvider['$get']>;

View file

@ -12,8 +12,8 @@ import 'angular-sanitize';
import 'angular-route';
import '../index.scss';
import { upperFirst } from 'lodash';
import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular';
import { CoreStart } from 'kibana/public';
import { i18nDirective, i18nFilter, I18nProvider } from './angular_i18n';
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
import {
createTopNavDirective,

View file

@ -5782,14 +5782,7 @@
dependencies:
"@turf/helpers" "6.x"
"@types/angular-mocks@^1.7.0":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@types/angular-mocks/-/angular-mocks-1.7.0.tgz#310d999a3c47c10ecd8eef466b5861df84799429"
integrity sha512-MeT5vxWBx4Ny5/sNZJjpZdv4K2KGwqQYiRQQZctan1TTaNyiVlFRYbcmheolhM4KKbTWmoxTVeuvGzniTDg1kw==
dependencies:
"@types/angular" "*"
"@types/angular@*", "@types/angular@^1.6.56":
"@types/angular@^1.6.56":
version "1.6.56"
resolved "https://registry.yarnpkg.com/@types/angular/-/angular-1.6.56.tgz#20124077bd44061e018c7283c0bb83f4b00322dd"
integrity sha512-HxtqilvklZ7i6XOaiP7uIJIrFXEVEhfbSY45nfv2DeBRngncI58Y4ZOUMiUkcT8sqgLL1ablmbfylChUg7A3GA==
@ -7873,11 +7866,6 @@ angular-aria@^1.8.0:
resolved "https://registry.yarnpkg.com/angular-aria/-/angular-aria-1.8.0.tgz#97aec9b1e8bafd07d5fab30f98d8ec832e18e25d"
integrity sha512-eCQI6EwgY6bYHdzIUfDABHnZjoZ3bNYpCsnceQF4bLfbq1QtZ7raRPNca45sj6C9Pfjde6PNcEDvuLozFPYnrQ==
angular-mocks@^1.7.9:
version "1.7.9"
resolved "https://registry.yarnpkg.com/angular-mocks/-/angular-mocks-1.7.9.tgz#0a3b7e28b9a493b4e3010ed2b0f69a68e9b4f79b"
integrity sha512-LQRqqiV3sZ7NTHBnNmLT0bXtE5e81t97+hkJ56oU0k3dqKv1s6F+nBWRlOVzqHWPGFOiPS8ZJVdrS8DFzHyNIA==
angular-recursion@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/angular-recursion/-/angular-recursion-1.0.5.tgz#cd405428a0bf55faf52eaa7988c1fe69cd930543"