[Canvas][i18n] Elements (#27904)

* [Canvas][i18n] Elements

* Addressing feedback; using global i18n

* Fixing unit test to reflect globals

* Making i18n more flexible

* Switching to a Provider strategy for i18n
This commit is contained in:
Clint Andrew Hall 2019-01-04 19:53:36 -06:00 committed by GitHub
parent 6cefed969b
commit fa475e2887
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 400 additions and 3 deletions

View file

@ -5,6 +5,14 @@
*/
import 'babel-polyfill';
import { applyElementStrings, i18nProvider } from '../strings';
import { elementSpecs } from './index';
elementSpecs.forEach(canvas.register);
const { i18n, register } = canvas;
// i18n is only available from Kibana when specs are registered. Init the Canvas i18n Provider with that instance.
i18nProvider.init(i18n);
// Apply localized strings to the Element specs, then register them.
applyElementStrings(elementSpecs).forEach(register);

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ElementFactory } from '../elements/types';
import { getElementStrings } from './index';
/**
* This function takes a set of Canvas Element specification factories, runs them,
* replaces relevant strings (if available) and returns a new factory. We do this
* so the specifications themselves have no dependency on i18n, for clarity for both
* our and external plugin developers.
*/
export const applyElementStrings = (elements: ElementFactory[]) => {
const elementStrings = getElementStrings();
return elements.map(spec => {
const result = spec();
const { name } = result;
const strings = elementStrings[name];
// If we have registered strings for this spec, we should replace any that are available.
if (strings) {
const { displayName, help } = strings;
// If the function has a registered help string, replace it on the spec.
if (help) {
result.help = help;
}
if (displayName) {
result.displayName = displayName;
}
}
return () => result;
});
};

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18nProvider } from './i18n_provider';
i18nProvider.init();
import { getElementStrings } from '.';
import { elementSpecs } from '../elements';
beforeAll(() => {
i18nProvider.init();
});
describe('ElementStrings', () => {
const elementStrings = getElementStrings();
const elementNames = elementSpecs.map(spec => spec().name);
const stringKeys = Object.keys(elementStrings);
test('All element names should exist in the strings definition', () => {
elementNames.forEach(name => expect(stringKeys).toContain(name));
});
test('All string definitions should correspond to an existing element', () => {
stringKeys.forEach(key => expect(elementNames).toContain(key));
});
const strings = Object.values(elementStrings);
test('All elements should have a displayName string defined', () => {
strings.forEach(value => {
expect(value).toHaveProperty('displayName');
});
});
test('All elements should have a help string defined', () => {
strings.forEach(value => {
expect(value).toHaveProperty('help');
});
});
});

View file

@ -0,0 +1,236 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18nProvider } from './i18n_provider';
interface ElementStrings {
displayName: string;
help: string;
}
interface ElementStringDict {
[elementName: string]: ElementStrings;
}
/**
* This function will return a dictionary of strings, organized by Canvas
* Element specification. This function requires that `i18nProvider` be
* properly initialized.
*/
export const getElementStrings = (): ElementStringDict => {
const i18n = i18nProvider.getInstance();
return {
areaChart: {
displayName: i18n.translate('xpack.canvas.elements.areaChartDisplayName', {
defaultMessage: 'Area chart',
}),
help: i18n.translate('xpack.canvas.elements.areaChartHelpText', {
defaultMessage: 'A line chart with a filled body',
}),
},
bubbleChart: {
displayName: i18n.translate('xpack.canvas.elements.bubbleChartDisplayName', {
defaultMessage: 'Bubble chart',
}),
help: i18n.translate('xpack.canvas.elements.bubbleChartHelpText', {
defaultMessage: 'A customizable bubble chart',
}),
},
debug: {
displayName: i18n.translate('xpack.canvas.elements.debugDisplayName', {
defaultMessage: 'Debug',
}),
help: i18n.translate('xpack.canvas.elements.debugHelpText', {
defaultMessage: 'Just dumps the configuration of the element',
}),
},
donut: {
displayName: i18n.translate('xpack.canvas.elements.donutChartDisplayName', {
defaultMessage: 'Donut chart',
}),
help: i18n.translate('xpack.canvas.elements.donutChartHelpText', {
defaultMessage: 'A customizable donut chart',
}),
},
dropdown_filter: {
displayName: i18n.translate('xpack.canvas.elements.dropdownFilterDisplayName', {
defaultMessage: 'Dropdown Filter',
}),
help: i18n.translate('xpack.canvas.elements.dropdownFilterHelpText', {
defaultMessage: 'A dropdown from which you can select values for an "exactly" filter',
}),
},
horizontalBarChart: {
displayName: i18n.translate('xpack.canvas.elements.horizontalBarChartDisplayName', {
defaultMessage: 'Horizontal Bar chart',
}),
help: i18n.translate('xpack.canvas.elements.horizontalBarChartHelpText', {
defaultMessage: 'A customizable horizontal bar chart',
}),
},
horizontalProgressBar: {
displayName: i18n.translate('xpack.canvas.elements.horizontalProgressBarDisplayName', {
defaultMessage: 'Horizontal Progress Bar',
}),
help: i18n.translate('xpack.canvas.elements.horizontalProgressBarHelpText', {
defaultMessage: 'Displays progress as a portion of a horizontal bar',
}),
},
horizontalProgressPill: {
displayName: i18n.translate('xpack.canvas.elements.horizontalProgressPillDisplayName', {
defaultMessage: 'Horizontal Progress Pill',
}),
help: i18n.translate('xpack.canvas.elements.horizontalProgressPillHelpText', {
defaultMessage: 'Displays progress as a portion of a horizontal pill',
}),
},
image: {
displayName: i18n.translate('xpack.canvas.elements.imageDisplayName', {
defaultMessage: 'Image',
}),
help: i18n.translate('xpack.canvas.elements.imageHelpText', {
defaultMessage: 'A static image',
}),
},
lineChart: {
displayName: i18n.translate('xpack.canvas.elements.lineChartDisplayName', {
defaultMessage: 'Line chart',
}),
help: i18n.translate('xpack.canvas.elements.lineChartHelpText', {
defaultMessage: 'A customizable line chart',
}),
},
markdown: {
displayName: i18n.translate('xpack.canvas.elements.markdownDisplayName', {
defaultMessage: 'Markdown',
}),
help: i18n.translate('xpack.canvas.elements.markdownHelpText', {
defaultMessage: 'Markup from Markdown',
}),
},
metric: {
displayName: i18n.translate('xpack.canvas.elements.metricDisplayName', {
defaultMessage: 'Metric',
}),
help: i18n.translate('xpack.canvas.elements.metricHelpText', {
defaultMessage: 'A number with a label',
}),
},
pie: {
displayName: i18n.translate('xpack.canvas.elements.pieDisplayName', {
defaultMessage: 'Pie chart',
}),
help: i18n.translate('xpack.canvas.elements.pieHelpText', {
defaultMessage: 'Pie chart',
}),
},
plot: {
displayName: i18n.translate('xpack.canvas.elements.plotDisplayName', {
defaultMessage: 'Coordinate plot',
}),
help: i18n.translate('xpack.canvas.elements.plotHelpText', {
defaultMessage: 'Mixed line, bar or dot charts',
}),
},
progressGauge: {
displayName: i18n.translate('xpack.canvas.elements.progressGaugeDisplayName', {
defaultMessage: 'Progress Gauge',
}),
help: i18n.translate('xpack.canvas.elements.progressGaugeHelpText', {
defaultMessage: 'Displays progress as a portion of a gauge',
}),
},
progressSemicircle: {
displayName: i18n.translate('xpack.canvas.elements.progressSemicircleDisplayName', {
defaultMessage: 'Progress Semicircle',
}),
help: i18n.translate('xpack.canvas.elements.progressSemicircleHelpText', {
defaultMessage: 'Displays progress as a portion of a semicircle',
}),
},
progressWheel: {
displayName: i18n.translate('xpack.canvas.elements.progressWheelDisplayName', {
defaultMessage: 'Progress Wheel',
}),
help: i18n.translate('xpack.canvas.elements.progressWheelHelpText', {
defaultMessage: 'Displays progress as a portion of a wheel',
}),
},
repeatImage: {
displayName: i18n.translate('xpack.canvas.elements.repeatImageDisplayName', {
defaultMessage: 'Image repeat',
}),
help: i18n.translate('xpack.canvas.elements.repeatImageHelpText', {
defaultMessage: 'Repeats an image N times',
}),
},
revealImage: {
displayName: i18n.translate('xpack.canvas.elements.revealImageDisplayName', {
defaultMessage: 'Image reveal',
}),
help: i18n.translate('xpack.canvas.elements.revealImageHelpText', {
defaultMessage: 'Reveals a percentage of an image',
}),
},
shape: {
displayName: i18n.translate('xpack.canvas.elements.shapeDisplayName', {
defaultMessage: 'Shape',
}),
help: i18n.translate('xpack.canvas.elements.shapeHelpText', {
defaultMessage: 'A customizable shape',
}),
},
table: {
displayName: i18n.translate('xpack.canvas.elements.tableDisplayName', {
defaultMessage: 'Data table',
}),
help: i18n.translate('xpack.canvas.elements.tableHelpText', {
defaultMessage: 'A scrollable grid for displaying data in a tabular format',
}),
},
tiltedPie: {
displayName: i18n.translate('xpack.canvas.elements.tiltedPieDisplayName', {
defaultMessage: 'Tilted pie chart',
}),
help: i18n.translate('xpack.canvas.elements.tiltedPieHelpText', {
defaultMessage: 'A customizable tilted pie chart',
}),
},
time_filter: {
displayName: i18n.translate('xpack.canvas.elements.timeFilterDisplayName', {
defaultMessage: 'Time filter',
}),
help: i18n.translate('xpack.canvas.elements.timeFilterHelpText', {
defaultMessage: 'Set a time window',
}),
},
verticalBarChart: {
displayName: i18n.translate('xpack.canvas.elements.verticalBarChartDisplayName', {
defaultMessage: 'Vertical bar chart',
}),
help: i18n.translate('xpack.canvas.elements.verticalBarChartHelpText', {
defaultMessage: 'A customizable vertical bar chart',
}),
},
verticalProgressBar: {
displayName: i18n.translate('xpack.canvas.elements.verticalProgressBarDisplayName', {
defaultMessage: 'Vertical Progress Bar',
}),
help: i18n.translate('xpack.canvas.elements.verticalProgressBarHelpText', {
defaultMessage: 'Displays progress as a portion of a vertical bar',
}),
},
verticalProgressPill: {
displayName: i18n.translate('xpack.canvas.elements.verticalProgressPillDisplayName', {
defaultMessage: 'Vertical Progress Pill',
}),
help: i18n.translate('xpack.canvas.elements.verticalProgressPillHelpText', {
defaultMessage: 'Displays progress as a portion of a vertical pill',
}),
},
};
};

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n as i18nCore } from '@kbn/i18n';
let i18nCoreInstance: typeof i18nCore | null = null;
/**
* @kbn/i18n is provided as a global module, but there's a difference between the version provided by Kibana, which is properly
* initialized, and the one imported directly from the global module. We need the former, as the latter, for example, won't
* be set to the proper locale, (set in kibana.yml or the command line).
*
* As a result, we need to initialize our own provider before using i18n in Canvas code. This simple singleton is here for that
* purpose.
*/
export const i18nProvider = {
// For simplicity in cases like testing, you can just init this Provider without parameters... but you won't have the
// Kibana-initialized i18n runtime.
init: (i18n: typeof i18nCore = i18nCore): typeof i18nCore => {
if (i18nCoreInstance === null) {
i18nCoreInstance = i18n;
}
return i18nCoreInstance;
},
getInstance: (): typeof i18nCore => {
if (i18nCoreInstance === null) {
throw new Error(
'i18nProvider not initialized; you must first call `init` with an instance of `@kbn/i18n`'
);
}
return i18nCoreInstance;
},
};

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './apply_strings';
export * from './element_strings';
export * from './i18n_provider';

View file

@ -25,11 +25,11 @@ export class Element {
public height?: number;
constructor(config: ElementSpec) {
const { name, image, displayName, expression, filter, width, height } = config;
const { name, image, displayName, expression, filter, help, width, height } = config;
this.name = name;
this.displayName = displayName || name;
this.image = image || defaultHeader;
this.help = config.help || '';
this.help = help || '';
if (!config.expression) {
throw new Error('Element types must have a default expression');

View file

@ -17,3 +17,9 @@ exports.runKibanaScript = function(name, args = []) {
process.argv.splice(2, 0, ...args);
require('../../../../scripts/' + name); // eslint-disable-line import/no-dynamic-require
};
exports.runXPackScript = function(name, args = []) {
process.chdir(resolve(__dirname, '../../..'));
process.argv.splice(2, 0, ...args);
require('../../../scripts/' + name); // eslint-disable-line import/no-dynamic-require
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
require('./_helpers').runXPackScript('jest', ['plugins/canvas']);

13
x-pack/plugins/canvas/types/global.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n as I18N } from '@kbn/i18n';
declare global {
const canvas: {
i18n: typeof I18N;
};
}