From 958a7c4d7aafb93e62db5bec59b87c976d0e6194 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Wed, 23 Jan 2019 14:17:47 -0700 Subject: [PATCH 1/2] Revert "Revert "EuiContext & I18n" (#1453)" This reverts commit 695f42113355ac10bb971eb2d66b67facaa90804. --- package.json | 2 +- .../babel/proptypes-from-ts-props/index.js | 165 +++++++++++++++--- .../proptypes-from-ts-props/index.test.js | 6 +- scripts/jest/setup/enzyme.js | 2 +- src-docs/src/routes.js | 8 + .../src/services/string/render_to_html.js | 2 +- src-docs/src/views/context/context.js | 94 ++++++++++ src-docs/src/views/context/context_example.js | 40 +++++ src-docs/src/views/i18n/i18n_basic.js | 16 ++ src-docs/src/views/i18n/i18n_example.js | 101 +++++++++++ src-docs/src/views/i18n/i18n_multi.js | 25 +++ src-docs/src/views/i18n/i18n_number.js | 13 ++ src-docs/src/views/i18n/i18n_renderprop.js | 22 +++ .../in_memory_table.test.js.snap | 16 +- src/components/context/context.tsx | 25 +++ src/components/context/index.ts | 4 + src/components/i18n/i18n.tsx | 52 ++++++ src/components/i18n/i18n_number.tsx | 47 +++++ src/components/i18n/index.ts | 2 + src/components/index.js | 10 ++ ...eld_value_toggle_group_filter.test.js.snap | 32 +++- .../table_pagination/table_pagination.js | 3 +- yarn.lock | 57 +++--- 23 files changed, 678 insertions(+), 66 deletions(-) create mode 100644 src-docs/src/views/context/context.js create mode 100644 src-docs/src/views/context/context_example.js create mode 100644 src-docs/src/views/i18n/i18n_basic.js create mode 100644 src-docs/src/views/i18n/i18n_example.js create mode 100644 src-docs/src/views/i18n/i18n_multi.js create mode 100644 src-docs/src/views/i18n/i18n_number.js create mode 100644 src-docs/src/views/i18n/i18n_renderprop.js create mode 100644 src/components/context/context.tsx create mode 100644 src/components/context/index.ts create mode 100644 src/components/i18n/i18n.tsx create mode 100644 src/components/i18n/i18n_number.tsx create mode 100644 src/components/i18n/index.ts diff --git a/package.json b/package.json index 1d5e79c5431..79ad8c24191 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "cssnano": "^4.0.5", "dts-generator": "^2.1.0", "enzyme": "^3.1.0", - "enzyme-adapter-react-16": "^1.0.2", + "enzyme-adapter-react-16.3": "^1.4.1", "enzyme-to-json": "^3.3.0", "eslint": "^4.9.0", "eslint-config-prettier": "^2.9.0", diff --git a/scripts/babel/proptypes-from-ts-props/index.js b/scripts/babel/proptypes-from-ts-props/index.js index a35cace5684..d5a75d5854e 100644 --- a/scripts/babel/proptypes-from-ts-props/index.js +++ b/scripts/babel/proptypes-from-ts-props/index.js @@ -127,6 +127,7 @@ function resolveIdentifierToPropTypes(node, state) { ); // PropTypes.node + case 'ReactChild': case 'ReactNode': return types.memberExpression( types.identifier('PropTypes'), @@ -139,7 +140,36 @@ function resolveIdentifierToPropTypes(node, state) { if (identifier.name === 'ExclusiveUnion') { // We use ExclusiveUnion at the top level to exclusively discriminate between types // propTypes itself must be an object so merge the union sets together as an intersection - return getPropTypesForNode( + + // Any types that are optional or non-existant on one side must be optional after the union + const aPropType = getPropTypesForNode(node.typeParameters.params[0], true, state); + const bPropType = getPropTypesForNode(node.typeParameters.params[1], true, state); + + const propsOnA = types.isCallExpression(aPropType) ? aPropType.arguments[0].properties : []; + const propsOnB = types.isCallExpression(bPropType) ? bPropType.arguments[0].properties : []; + + // optional props is any prop that is optional or non-existant on one side + const optionalProps = new Set(); + for (let i = 0; i < propsOnA.length; i++) { + const property = propsOnA[i]; + const propertyName = property.key.name; + const isOptional = !isPropTypeRequired(types, property.value); + const existsOnB = propsOnB.find(property => property.key.name === propertyName) != null; + if (isOptional || !existsOnB) { + optionalProps.add(propertyName); + } + } + for (let i = 0; i < propsOnB.length; i++) { + const property = propsOnB[i]; + const propertyName = property.key.name; + const isOptional = !isPropTypeRequired(types, property.value); + const existsOnA = propsOnA.find(property => property.key.name === propertyName) != null; + if (isOptional || !existsOnA) { + optionalProps.add(propertyName); + } + } + + const propTypes = getPropTypesForNode( { type: 'TSIntersectionType', types: node.typeParameters.params, @@ -147,6 +177,19 @@ function resolveIdentifierToPropTypes(node, state) { true, state ); + + if (types.isCallExpression(propTypes)) { + // apply the optionals + const properties = propTypes.arguments[0].properties; + for (let i = 0; i < properties.length; i++) { + const property = properties[i]; + if (optionalProps.has(property.key.name)) { + property.value = makePropTypeOptional(types, property.value); + } + } + } + + return propTypes; } // Lookup this identifier from types/interfaces defined in code @@ -172,6 +215,41 @@ function buildPropTypePrimitiveExpression(types, typeName) { ); } +function isPropTypeRequired(types, propType) { + return types.isMemberExpression(propType) && + types.isIdentifier(propType.property) && + propType.property.name === 'isRequired'; +} + +function makePropTypeRequired(types, propType) { + return types.memberExpression( + propType, + types.identifier('isRequired') + ); +} + +function makePropTypeOptional(types, propType) { + if (isPropTypeRequired(types, propType)) { + // strip the .isRequired member expression + return propType.object; + } + return propType; +} + +function areExpressionsIdentical(a, b) { + const aCode = babelCore.transformFromAst(babelCore.types.program([ + babelCore.types.expressionStatement( + babelCore.types.removeComments(babelCore.types.cloneDeep(a)) + ) + ])).code; + const bCode = babelCore.transformFromAst(babelCore.types.program([ + babelCore.types.expressionStatement( + babelCore.types.removeComments(babelCore.types.cloneDeep(b)) + ) + ])).code; + return aCode === bCode; +} + /** * Heavy lifter to generate the proptype AST for a node. Initially called by `processComponentDeclaration`, * its return value is set as the component's `propTypes` value. This function calls itself recursively to translate @@ -202,31 +280,35 @@ function getPropTypesForNode(node, optional, state) { // translates intersections (Foo & Bar & Baz) to a shape with the types' members (Foo, Bar, Baz) merged together case 'TSIntersectionType': + const usableNodes = node.types.filter(node => { + const nodePropTypes = getPropTypesForNode(node, true, state); + + if ( + types.isMemberExpression(nodePropTypes) && + nodePropTypes.object.name === 'PropTypes' && + nodePropTypes.property.name === 'any' + ) { + return false; + } + + // validate that this resulted in a shape, otherwise we don't know how to extract/merge the values + if ( + !types.isCallExpression(nodePropTypes) || + !types.isMemberExpression(nodePropTypes.callee) || + nodePropTypes.callee.object.name !== 'PropTypes' || + nodePropTypes.callee.property.name !== 'shape' + ) { + return false; + } + + return true; + }); + // merge the resolved proptypes for each intersection member into one object, mergedProperties - const mergedProperties = node.types.reduce( + const mergedProperties = usableNodes.reduce( (mergedProperties, node) => { const nodePropTypes = getPropTypesForNode(node, true, state); - // if this propType is PropTypes.any there is nothing to do here - if ( - types.isMemberExpression(nodePropTypes) && - nodePropTypes.object.name === 'PropTypes' && - nodePropTypes.property.name === 'any' - ) { - return mergedProperties; - } - - // validate that this resulted in a shape, otherwise we don't know how to extract/merge the values - if ( - !types.isCallExpression(nodePropTypes) || - !types.isMemberExpression(nodePropTypes.callee) || - nodePropTypes.callee.object.name !== 'PropTypes' || - nodePropTypes.callee.property.name !== 'shape' - ) { - return mergedProperties; - // throw new Error('Cannot process an encountered type intersection'); - } - // iterate over this type's members, adding them (and their comments) to `mergedProperties` const typeProperties = nodePropTypes.arguments[0].properties; // properties on the ObjectExpression passed to PropTypes.shape() for (let i = 0; i < typeProperties.length; i++) { @@ -237,7 +319,32 @@ function getPropTypesForNode(node, optional, state) { ...(typeProperty.leadingComments || []), ...((mergedProperties[typeProperty.key.name] ? mergedProperties[typeProperty.key.name].leadingComments : null) || []), ]; - mergedProperties[typeProperty.key.name] = typeProperty.value; + + // if this property has already been found, the only action is to potentially change it to optional + if (mergedProperties.hasOwnProperty(typeProperty.key.name)) { + const existing = mergedProperties[typeProperty.key.name]; + if (!areExpressionsIdentical(existing, typeProperty.value)) { + mergedProperties[typeProperty.key.name] = types.callExpression( + types.memberExpression( + types.identifier('PropTypes'), + types.identifier('oneOfType'), + ), + [ + types.arrayExpression( + [existing, typeProperty.value] + ) + ] + ); + + if (isPropTypeRequired(types, existing) && isPropTypeRequired(types, typeProperty.value)) { + mergedProperties[typeProperty.key.name] = makePropTypeRequired(types, mergedProperties[typeProperty.key.name]); + } + } + } else { + // property hasn't been seen yet, add it + mergedProperties[typeProperty.key.name] = typeProperty.value; + } + mergedProperties[typeProperty.key.name].leadingComments = leadingComments; } @@ -346,6 +453,9 @@ function getPropTypesForNode(node, optional, state) { [ types.objectExpression( node.members.map(property => { + // skip TS index signatures + if (types.isTSIndexSignature(property)) return null; + const objectProperty = types.objectProperty( types.identifier(property.key.name || `"${property.key.value}"`), getPropTypesForNode(property.typeAnnotation, property.optional, state) @@ -354,7 +464,7 @@ function getPropTypesForNode(node, optional, state) { objectProperty.leadingComments = property.leadingComments.map(({ type, value }) => ({ type, value })); } return objectProperty; - }) + }).filter(x => x != null) ) ] ); @@ -548,10 +658,7 @@ function getPropTypesForNode(node, optional, state) { if (optional) { return propType; } else { - return types.memberExpression( - propType, - types.identifier('isRequired') - ); + return makePropTypeRequired(types, propType); } } @@ -967,7 +1074,7 @@ module.exports = function propTypesFromTypeScript({ types }) { processComponentDeclaration(idTypeAnnotation.typeAnnotation.typeParameters.params[0], nodePath, state); fileCodeNeedsUpdating = true; } else { - throw new Error(`Cannot process annotation id React.${right.name}`); + // throw new Error(`Cannot process annotation id React.${right.name}`); } } } else if (idTypeAnnotation.typeAnnotation.typeName.type === 'Identifier') { diff --git a/scripts/babel/proptypes-from-ts-props/index.test.js b/scripts/babel/proptypes-from-ts-props/index.test.js index 77a4b7d88f1..e6bea1f615b 100644 --- a/scripts/babel/proptypes-from-ts-props/index.test.js +++ b/scripts/babel/proptypes-from-ts-props/index.test.js @@ -984,7 +984,7 @@ import React from 'react'; export type ExclusiveUnion = any; interface BaseProps { asdf: boolean } interface IFooProps extends BaseProps {d: number, foo?: string} -interface IBarProps extends BaseProps {d: string, bar?: string} +interface IBarProps extends BaseProps {d: string, foo: string, bar: string} const FooComponent: React.SFC> = () => { return (
Hello World
); }`, @@ -999,8 +999,8 @@ const FooComponent = () => { }; FooComponent.propTypes = { - d: PropTypes.string.isRequired, - foo: PropTypes.string, + d: PropTypes.oneOfType([PropTypes.number.isRequired, PropTypes.string.isRequired]).isRequired, + foo: PropTypes.oneOfType([PropTypes.string, PropTypes.string.isRequired]), asdf: PropTypes.bool.isRequired, bar: PropTypes.string };`); diff --git a/scripts/jest/setup/enzyme.js b/scripts/jest/setup/enzyme.js index 82edfc9e5ad..7c4def46489 100644 --- a/scripts/jest/setup/enzyme.js +++ b/scripts/jest/setup/enzyme.js @@ -1,4 +1,4 @@ import { configure } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; +import Adapter from 'enzyme-adapter-react-16.3'; configure({ adapter: new Adapter() }); diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index f11cde47578..6db4e77e6c6 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -90,6 +90,9 @@ import { ColorPickerExample } import { ComboBoxExample } from './views/combo_box/combo_box_example'; +import { ContextExample } + from './views/context/context_example'; + import { ContextMenuExample } from './views/context_menu/context_menu_example'; @@ -147,6 +150,9 @@ import { HighlightExample } import { HorizontalRuleExample } from './views/horizontal_rule/horizontal_rule_example'; +import { I18nExample } + from './views/i18n/i18n_example'; + import { IconExample } from './views/icon/icon_example'; @@ -413,11 +419,13 @@ const navigation = [{ items: [ AccessibilityExample, ColorPaletteExample, + ContextExample, CopyExample, UtilityClassesExample, DelayHideExample, ErrorBoundaryExample, HighlightExample, + I18nExample, IsColorDarkExample, MutationObserverExample, OutsideClickDetectorExample, diff --git a/src-docs/src/services/string/render_to_html.js b/src-docs/src/services/string/render_to_html.js index 1cd47b401c0..f632809b77b 100644 --- a/src-docs/src/services/string/render_to_html.js +++ b/src-docs/src/services/string/render_to_html.js @@ -5,7 +5,7 @@ import { configure } from 'enzyme'; -import EnzymeAdapter from 'enzyme-adapter-react-16'; +import EnzymeAdapter from 'enzyme-adapter-react-16.3'; import html from 'html'; diff --git a/src-docs/src/views/context/context.js b/src-docs/src/views/context/context.js new file mode 100644 index 00000000000..8bdb0b7506d --- /dev/null +++ b/src-docs/src/views/context/context.js @@ -0,0 +1,94 @@ +import React, { Component, Fragment } from 'react'; + +import { + EuiContext, + EuiButton, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, + EuiI18n, + EuiI18nNumber, +} from '../../../../src/components'; + +const mappings = { + fr: { + english: 'Anglais', + french: 'Française', + greeting: 'Salutations!', + guestNo: 'Vous êtes invité #', + question: 'Quel est votre nom?', + placeholder: 'Jean Dupont', + action: 'Soumettre', + }, +}; + +export default class extends Component { + state = { + language: 'en', + name: '' + } + + setLanguage = (language) => this.setState({ language }) + + render() { + const i18n = { + mapping: mappings[this.state.language], + formatNumber: (value) => new Intl.NumberFormat(this.state.language).format(value), + }; + + return ( + +
+ + + this.setLanguage('en')}> + + + + + + this.setLanguage('fr')}> + + + + + + + + + + + +

+ + + + + {([question, action]) => ( + + + + + {placeholder => ( + + )} + + + + + {action} + + )} + +
+
+ ); + } +} diff --git a/src-docs/src/views/context/context_example.js b/src-docs/src/views/context/context_example.js new file mode 100644 index 00000000000..2308cb6fe51 --- /dev/null +++ b/src-docs/src/views/context/context_example.js @@ -0,0 +1,40 @@ +import React from 'react'; + +import { renderToHtml } from '../../services'; + +import { + GuideSectionTypes, +} from '../../components'; + +import { + EuiCode, + EuiContext, + EuiI18n, +} from '../../../../src/components'; + +import Context from './context'; +const contextSource = require('!!raw-loader!./context'); +const contextHtml = renderToHtml(Context); + +export const ContextExample = { + title: 'Context', + sections: [{ + source: [{ + type: GuideSectionTypes.JS, + code: contextSource, + }, { + type: GuideSectionTypes.HTML, + code: contextHtml, + }], + text: ( +

+ EuiContext allows setting global internationalization copy for + EUI components. Any components used within this context will lookup their display values + from this mapping. +

+ ), + components: { EuiContext }, + demo: , + props: { EuiContext, EuiI18n }, + }], +}; diff --git a/src-docs/src/views/i18n/i18n_basic.js b/src-docs/src/views/i18n/i18n_basic.js new file mode 100644 index 00000000000..4fa95c923a6 --- /dev/null +++ b/src-docs/src/views/i18n/i18n_basic.js @@ -0,0 +1,16 @@ +import React from 'react'; + +import { + EuiI18n, +} from '../../../../src/components'; + +export default () => { + return ( +

+ +

+ ); +}; diff --git a/src-docs/src/views/i18n/i18n_example.js b/src-docs/src/views/i18n/i18n_example.js new file mode 100644 index 00000000000..3d9db2fba43 --- /dev/null +++ b/src-docs/src/views/i18n/i18n_example.js @@ -0,0 +1,101 @@ +import React from 'react'; + +import { renderToHtml } from '../../services'; + +import { + GuideSectionTypes, +} from '../../components'; + +import { + EuiCode, + EuiI18n, +} from '../../../../src/components'; + +import I18nBasic from './i18n_basic'; +const i18nBasicSource = require('!!raw-loader!./i18n_basic'); +const i18nBasicHtml = renderToHtml(I18nBasic); + +import I18nRenderProp from './i18n_renderprop'; +const i18nRenderPropSource = require('!!raw-loader!./i18n_renderprop'); +const i18nRenderPropHtml = renderToHtml(I18nRenderProp); + +import I18nMulti from './i18n_multi'; +const I18nMultiSource = require('!!raw-loader!./i18n_multi'); +const I18nMultiHtml = renderToHtml(I18nMulti); + +import I18nNumber from './i18n_number'; +const I18nNumberSource = require('!!raw-loader!./i18n_number'); +const I18nNumberHtml = renderToHtml(I18nNumber); + +export const I18nExample = { + title: 'I18n', + sections: [{ + source: [{ + type: GuideSectionTypes.JS, + code: i18nBasicSource, + }, { + type: GuideSectionTypes.HTML, + code: i18nBasicHtml, + }], + text: ( +

+ EuiI18n allows localizing string and numeric values for internationalization. At its simplest, + the component takes token and default props.  + token provides a reference to use when looking for a localized value to render + and default provides the untranslated value. +

+ ), + demo: , + props: { EuiI18n }, + }, { + title: 'As a render prop', + source: [{ + type: GuideSectionTypes.JS, + code: i18nRenderPropSource, + }, { + type: GuideSectionTypes.HTML, + code: i18nRenderPropHtml, + }], + text: ( +

+ Some times a localized value is needed for a prop instead of rendering directly to the DOM. In + these cases EuiI18n can be passed a render prop child which is called with the localized value. +

+ ), + demo: , + }, { + title: 'Multi-value lookup', + source: [{ + type: GuideSectionTypes.JS, + code: I18nMultiSource, + }, { + type: GuideSectionTypes.HTML, + code: I18nMultiHtml, + }], + text: ( +

+ If many localized values are needed in a small area, multiple tokens can be retrieved in a single render prop. + In this case the token/default props are replaced + by the pluralized tokens/defaults. +

+ ), + demo: , + }, { + title: 'Number localization', + source: [{ + type: GuideSectionTypes.JS, + code: I18nNumberSource, + }, { + type: GuideSectionTypes.HTML, + code: I18nNumberHtml, + }], + text: ( +

+ EuiI18nNumber can be used to format one or more numbers. Similarly + to EuiI18n, it takes value or + values and can render directly to the DOM or call a render prop. +

+ ), + demo: , + }], +}; diff --git a/src-docs/src/views/i18n/i18n_multi.js b/src-docs/src/views/i18n/i18n_multi.js new file mode 100644 index 00000000000..35a93f4bb49 --- /dev/null +++ b/src-docs/src/views/i18n/i18n_multi.js @@ -0,0 +1,25 @@ +import React from 'react'; + +import { + EuiCard, + EuiCode, + EuiI18n, +} from '../../../../src/components'; + +export default () => { + return ( +
+

+ Both title and description for the card are looked up in one call to EuiI18n +

+ + {([title, description]) => ( + + )} + +
+ ); +}; diff --git a/src-docs/src/views/i18n/i18n_number.js b/src-docs/src/views/i18n/i18n_number.js new file mode 100644 index 00000000000..d1b5775216b --- /dev/null +++ b/src-docs/src/views/i18n/i18n_number.js @@ -0,0 +1,13 @@ +import React from 'react'; + +import { + EuiI18nNumber, +} from '../../../../src/components'; + +export default () => { + return ( +

+ Formatted count of users: +

+ ); +}; diff --git a/src-docs/src/views/i18n/i18n_renderprop.js b/src-docs/src/views/i18n/i18n_renderprop.js new file mode 100644 index 00000000000..68e518a25da --- /dev/null +++ b/src-docs/src/views/i18n/i18n_renderprop.js @@ -0,0 +1,22 @@ +import React, { Fragment } from 'react'; + +import { + EuiCode, + EuiFieldText, + EuiI18n, +} from '../../../../src/components'; + +export default () => { + return ( + +

+ This text field's placeholder reads from i18n.renderpropexample +

+
+ + {placeholderName => } + +
+
+ ); +}; diff --git a/src/components/basic_table/__snapshots__/in_memory_table.test.js.snap b/src/components/basic_table/__snapshots__/in_memory_table.test.js.snap index d32d002701a..6a959585cc2 100644 --- a/src/components/basic_table/__snapshots__/in_memory_table.test.js.snap +++ b/src/components/basic_table/__snapshots__/in_memory_table.test.js.snap @@ -283,7 +283,12 @@ exports[`EuiInMemoryTable behavior pagination 1`] = ` size="xs" type="button" > - Rows per page: 2 + + : + 2 } closePopover={[Function]} @@ -366,7 +371,14 @@ exports[`EuiInMemoryTable behavior pagination 1`] = ` - Rows per page: 2 + + Rows per page + + : + 2 diff --git a/src/components/context/context.tsx b/src/components/context/context.tsx new file mode 100644 index 00000000000..be1fb75aa3e --- /dev/null +++ b/src/components/context/context.tsx @@ -0,0 +1,25 @@ +import React, { createContext, ReactChild } from 'react'; + +export interface I18nShape { + mapping?: { + [key: string]: ReactChild; + }; + formatNumber?: (x: number) => string; + formatDateTime?: (x: Date) => string; +} + +const I18nContext: React.Context = createContext({}); +const { Provider: EuiI18nProvider, Consumer: EuiI18nConsumer } = I18nContext; + +interface IEuiContextProps { + i18n: I18nShape; + children: React.ReactNode; +} + +const EuiContext: React.SFC = ({i18n = {}, children}) => ( + + {children} + +); + +export { EuiContext, EuiI18nConsumer }; diff --git a/src/components/context/index.ts b/src/components/context/index.ts new file mode 100644 index 00000000000..4f56b7e8adf --- /dev/null +++ b/src/components/context/index.ts @@ -0,0 +1,4 @@ +export { + EuiContext, + EuiI18nConsumer +} from './context'; diff --git a/src/components/i18n/i18n.tsx b/src/components/i18n/i18n.tsx new file mode 100644 index 00000000000..4530104d598 --- /dev/null +++ b/src/components/i18n/i18n.tsx @@ -0,0 +1,52 @@ +import React, { ReactChild, ReactElement } from 'react'; +import { EuiI18nConsumer } from '../context'; +import { ExclusiveUnion } from '../common'; +import { I18nShape } from '../context/context'; + +// +// {(foo) =>

foo

}
+// {([foo, bar]) =>

{foo}, {bar}

+ +function lookupToken(token: string, i18nMapping: I18nShape['mapping'], valueDefault: ReactChild) { + return (i18nMapping && i18nMapping[token]) || valueDefault; +} + +interface I18nTokenShape { + token: string; + default: ReactChild; + children?: (x: ReactChild) => ReactElement; +} + +interface I18nTokensShape { + tokens: string[]; + defaults: ReactChild[]; + children: (x: ReactChild[]) => ReactElement; +} + +type EuiI18nProps = ExclusiveUnion; + +function hasTokens(x: EuiI18nProps): x is I18nTokensShape { + return x.tokens != null; +} + +const EuiI18n: React.SFC = (props) => ( + + { + (i18nConfig) => { + const { mapping } = i18nConfig; + if (hasTokens(props)) { + return props.children(props.tokens.map((token, idx) => lookupToken(token, mapping, props.defaults[idx]))); + } + + const tokenValue = lookupToken(props.token, mapping, props.default); + if (props.children) { + return props.children(tokenValue); + } else { + return tokenValue; + } + } + } + +); + +export { EuiI18n }; diff --git a/src/components/i18n/i18n_number.tsx b/src/components/i18n/i18n_number.tsx new file mode 100644 index 00000000000..412f43fb5c4 --- /dev/null +++ b/src/components/i18n/i18n_number.tsx @@ -0,0 +1,47 @@ +import React, { ReactChild, ReactElement } from 'react'; +import { EuiI18nConsumer } from '../context'; +import { ExclusiveUnion } from '../common'; + +const defaultFormatter = new Intl.NumberFormat('en'); +function defaultFormatNumber(value: number) { + return defaultFormatter.format(value); +} + +interface EuiI18nNumberValueShape { + value: number; + children?: (x: ReactChild) => ReactElement; +} + +interface EuiI18nNumberValuesShape { + values: number[]; + children: (x: ReactChild[]) => ReactElement; +} + +type EuiI18nNumberProps = ExclusiveUnion; + +function hasValues(x: EuiI18nNumberProps): x is EuiI18nNumberValuesShape { + return x.values != null; +} + +const EuiI18nNumber: React.SFC = (props) => ( + + { + (i18nConfig) => { + const formatNumber = i18nConfig.formatNumber || defaultFormatNumber; + + if (hasValues(props)) { + return props.children(props.values.map(value => formatNumber(value))); + } + + const formattedValue = (formatNumber || defaultFormatNumber)(props.value); + if (props.children) { + return props.children(formattedValue); + } else { + return formattedValue; + } + } + } + +); + +export { EuiI18nNumber }; diff --git a/src/components/i18n/index.ts b/src/components/i18n/index.ts new file mode 100644 index 00000000000..900959c7cae --- /dev/null +++ b/src/components/i18n/index.ts @@ -0,0 +1,2 @@ +export { EuiI18n } from './i18n'; +export { EuiI18nNumber } from './i18n_number'; diff --git a/src/components/index.js b/src/components/index.js index 9d39dd18f68..654bed85b61 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -60,6 +60,11 @@ export { EuiComboBox, } from './combo_box'; +export { + EuiContext, + EuiI18nConsumer +} from './context'; + export { EuiContextMenu, EuiContextMenuPanel, @@ -181,6 +186,11 @@ export { EuiImage, } from './image'; +export { + EuiI18n, + EuiI18nNumber, +} from './i18n'; + export { EuiLoadingKibana, EuiLoadingChart, diff --git a/src/components/search_bar/filters/__snapshots__/field_value_toggle_group_filter.test.js.snap b/src/components/search_bar/filters/__snapshots__/field_value_toggle_group_filter.test.js.snap index ca46513be20..f9694849c33 100644 --- a/src/components/search_bar/filters/__snapshots__/field_value_toggle_group_filter.test.js.snap +++ b/src/components/search_bar/filters/__snapshots__/field_value_toggle_group_filter.test.js.snap @@ -11,7 +11,9 @@ Array [ noDivider={true} onClick={[Function]} type="button" - />, + > + Kibana + , , + > + Elasticsearch + , ] `; @@ -36,7 +40,9 @@ Array [ noDivider={true} onClick={[Function]} type="button" - />, + > + -Kibana + , , + > + Elasticsearch + , ] `; @@ -61,7 +69,9 @@ Array [ noDivider={true} onClick={[Function]} type="button" - />, + > + Not Kibana + , , + > + Elasticsearch + , ] `; @@ -86,7 +98,9 @@ Array [ noDivider={true} onClick={[Function]} type="button" - />, + > + Kibana + , , + > + Elasticsearch + , ] `; diff --git a/src/components/table/table_pagination/table_pagination.js b/src/components/table/table_pagination/table_pagination.js index cccbf78d0f8..002f75a6351 100644 --- a/src/components/table/table_pagination/table_pagination.js +++ b/src/components/table/table_pagination/table_pagination.js @@ -8,6 +8,7 @@ import { EuiContextMenuItem, EuiContextMenuPanel } from '../../context_menu'; import { EuiFlexGroup, EuiFlexItem } from '../../flex'; import { EuiPagination } from '../../pagination'; import { EuiPopover } from '../../popover'; +import { EuiI18n } from '../../i18n'; export class EuiTablePagination extends Component { constructor(props) { @@ -49,7 +50,7 @@ export class EuiTablePagination extends Component { iconSide="right" onClick={this.onButtonClick} > - {`Rows per page: ${itemsPerPage}`} + : {itemsPerPage} ); diff --git a/yarn.lock b/yarn.lock index fd44dc3f2a4..a18daba0bdf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4275,27 +4275,29 @@ entities@^1.1.1, entities@~1.1.1: resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" integrity sha1-blwtClYhtdra7O+AuQ7ftc13cvA= -enzyme-adapter-react-16@^1.0.2: - version "1.1.1" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.1.1.tgz#a8f4278b47e082fbca14f5bfb1ee50ee650717b4" - integrity sha512-kC8pAtU2Jk3OJ0EG8Y2813dg9Ol0TXi7UNxHzHiWs30Jo/hj7alc//G1YpKUsPP1oKl9X+Lkx+WlGJpPYA+nvw== +enzyme-adapter-react-16.3@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16.3/-/enzyme-adapter-react-16.3-1.4.1.tgz#7da00e529ddecf62aafd4f7cda132c13f18c99ee" + integrity sha512-5jhavjTSid0KqW0pPKDZV3CpNX3Pfw6Urwbz/Lt8k1iX5qnxh+7hbICjbEK9IGh5FeMQMO5VGOpmFk77C+EL/A== dependencies: - enzyme-adapter-utils "^1.3.0" - lodash "^4.17.4" - object.assign "^4.0.4" + enzyme-adapter-utils "^1.9.0" + function.prototype.name "^1.1.0" + object.assign "^4.1.0" object.values "^1.0.4" - prop-types "^15.6.0" + prop-types "^15.6.2" + react-is "^16.6.1" react-reconciler "^0.7.0" - react-test-renderer "^16.0.0-0" + react-test-renderer "~16.3.0-0" -enzyme-adapter-utils@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.3.0.tgz#d6c85756826c257a8544d362cc7a67e97ea698c7" - integrity sha512-vVXSt6uDv230DIv+ebCG66T1Pm36Kv+m74L1TrF4kaE7e1V7Q/LcxO0QRkajk5cA6R3uu9wJf5h13wOTezTbjA== +enzyme-adapter-utils@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.9.1.tgz#68196fdaf2a9f51f31603cbae874618661233d72" + integrity sha512-LWc88BbKztLXlpRf5Ba/pSMJRaNezAwZBvis3N/IuB65ltZEh2E2obWU9B36pAbw7rORYeBUuqc79OL17ZzN1A== dependencies: - lodash "^4.17.4" - object.assign "^4.0.4" - prop-types "^15.6.0" + function.prototype.name "^1.1.0" + object.assign "^4.1.0" + prop-types "^15.6.2" + semver "^5.6.0" enzyme-to-json@^3.3.0: version "3.3.1" @@ -5594,7 +5596,7 @@ function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -function.prototype.name@^1.0.3: +function.prototype.name@^1.0.3, function.prototype.name@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.0.tgz#8bd763cc0af860a859cc5d49384d74b932cd2327" integrity sha512-Bs0VRrTz4ghD8pTmbJQD1mZ8A/mN0ur/jGz+A6FBxPDUPkm1tNfF6bhTYPA7i7aF4lZJVr+OXTNNrnnIl58Wfg== @@ -9465,7 +9467,7 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@^4.0.3, object.assign@^4.0.4, object.assign@^4.1.0: +object.assign@^4.0.3, object.assign@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== @@ -10902,7 +10904,7 @@ prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.6.0: loose-envify "^1.3.1" object-assign "^4.1.1" -prop-types@^15.5.8: +prop-types@^15.5.8, prop-types@^15.6.2: version "15.6.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ== @@ -11204,6 +11206,11 @@ react-input-autosize@^2.2.1: dependencies: prop-types "^15.5.8" +react-is@^16.3.2, react-is@^16.6.1: + version "16.7.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.7.0.tgz#c1bd21c64f1f1364c6f70695ec02d69392f41bfa" + integrity sha512-Z0VRQdF4NPDoI0tsXVMLkJLiwEBa+RP66g0xDHxgxysxSoCUccSten4RTF/UFvZF1dZvZ9Zu1sx+MDXwcOR34g== + react-motion@^0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316" @@ -11253,7 +11260,7 @@ react-router@^3.2.0: prop-types "^15.5.6" warning "^3.0.0" -react-test-renderer@^16.0.0-0, react-test-renderer@^16.2.0: +react-test-renderer@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.2.0.tgz#bddf259a6b8fcd8555f012afc8eacc238872a211" integrity sha512-Kd4gJFtpNziR9ElOE/C23LeflKLZPRpNQYWP3nQBY43SJ5a+xyEGSeMrm2zxNKXcnCbBS/q1UpD9gqd5Dv+rew== @@ -11262,6 +11269,16 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.2.0: object-assign "^4.1.1" prop-types "^15.6.0" +react-test-renderer@~16.3.0-0: + version "16.3.2" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.3.2.tgz#3d1ed74fda8db42521fdf03328e933312214749a" + integrity sha512-lL8WHIpCTMdSe+CRkt0rfMxBkJFyhVrpdQ54BaJRIrXf9aVmbeHbRA8GFRpTvohPN5tPzMabmrzW2PUfWCfWwQ== + dependencies: + fbjs "^0.8.16" + object-assign "^4.1.1" + prop-types "^15.6.0" + react-is "^16.3.2" + react-virtualized@^9.18.5: version "9.18.5" resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.18.5.tgz#42dd390ebaa7ea809bfcaf775d39872641679b89" From 978dd0f01edc17acbadc34f9cca0c7dd1d7a2d7a Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Wed, 23 Jan 2019 14:21:22 -0700 Subject: [PATCH 2/2] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 700433517a7..c720c0451c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## [`master`](https://github.com/elastic/eui/tree/master) -No public interface changes since `6.6.0`. +- Re-added EuiI18n, EuiI18nNumber, and EuiContext for localization ([#1466](https://github.com/elastic/eui/pull/1466)) ## [`6.6.0`](https://github.com/elastic/eui/tree/v6.6.0)