From a2338d9eede79ef35463d68bac7ddb3acab5fd62 Mon Sep 17 00:00:00 2001
From: Chandler Prall
Date: Wed, 15 Jul 2020 13:31:46 -0600
Subject: [PATCH 1/7] Added useEuiI18n hook
---
src/components/context/context.tsx | 2 +-
.../i18n/__snapshots__/i18n.test.tsx.snap | 110 +++++++++++++++
src/components/i18n/i18n.test.tsx | 133 +++++++++++++++++-
src/components/i18n/i18n.tsx | 45 +++++-
src/components/i18n/index.ts | 2 +-
5 files changed, 284 insertions(+), 8 deletions(-)
diff --git a/src/components/context/context.tsx b/src/components/context/context.tsx
index 02c55c75aab..abd0a6cd57a 100644
--- a/src/components/context/context.tsx
+++ b/src/components/context/context.tsx
@@ -48,4 +48,4 @@ const EuiContext: React.FunctionComponent = ({
children,
}) => {children} ;
-export { EuiContext, EuiI18nConsumer };
+export { EuiContext, EuiI18nConsumer, I18nContext };
diff --git a/src/components/i18n/__snapshots__/i18n.test.tsx.snap b/src/components/i18n/__snapshots__/i18n.test.tsx.snap
index 462989707db..f099e8aa0d2 100644
--- a/src/components/i18n/__snapshots__/i18n.test.tsx.snap
+++ b/src/components/i18n/__snapshots__/i18n.test.tsx.snap
@@ -151,6 +151,87 @@ exports[`EuiI18n default rendering rendering to dom renders when value is null 1
`;
+exports[`EuiI18n mapped tokens handles multiple tokens 1`] = `
+
+
+
+
+ first value
+
+
+ second value
+
+
+
+
+`;
+
+exports[`EuiI18n mapped tokens handles single token with values 1`] = `
+
+
+
+ In reverse order: aardvarks, then apples
+
+
+
+`;
+
+exports[`EuiI18n mapped tokens handles single token without values 1`] = `
+
+
+
+ This is the mapped value.
+
+
+
+`;
+
+exports[`EuiI18n mapped tokens mappingFunc calls the mapping function with the source string 1`] = `
+
+
+
+ PLACEHOLDER
+
+
+
+`;
+
exports[`EuiI18n reading values from context mappingFunc calls the mapping function with the source string 1`] = `
`;
+
+exports[`EuiI18n useEuiI18n unmapped handles multiple tokens 1`] = `
+
+
+
+ the first placeholder
+
+
+ the second placeholder
+
+
+
+`;
+
+exports[`EuiI18n useEuiI18n unmapped handles single token with values 1`] = `
+
+
+ first apples, then aardvarks
+
+
+`;
+
+exports[`EuiI18n useEuiI18n unmapped handles single token without values 1`] = `
+
+
+ placeholder
+
+
+`;
diff --git a/src/components/i18n/i18n.test.tsx b/src/components/i18n/i18n.test.tsx
index b5ced702768..626280e32e5 100644
--- a/src/components/i18n/i18n.test.tsx
+++ b/src/components/i18n/i18n.test.tsx
@@ -20,7 +20,7 @@
import React, { ReactChild } from 'react';
import { mount } from 'enzyme';
import { EuiContext } from '../context';
-import { EuiI18n } from './i18n';
+import { EuiI18n, useEuiI18n } from './i18n';
/* eslint-disable local/i18n */
@@ -262,4 +262,135 @@ describe('EuiI18n', () => {
});
});
});
+
+ describe('useEuiI18n', () => {
+ describe('unmapped', () => {
+ it('handles single token without values', () => {
+ const Component = () => {
+ const value = useEuiI18n('token', 'placeholder');
+ return {value}
;
+ };
+ const component = mount( );
+ expect(component).toMatchSnapshot();
+ });
+
+ it('handles single token with values', () => {
+ const Component = () => {
+ const value = useEuiI18n('myToken', 'first {first}, then {second}', {
+ first: 'apples',
+ second: 'aardvarks',
+ });
+ return {value}
;
+ };
+ const component = mount( );
+ expect(component).toMatchSnapshot();
+ });
+
+ it('handles multiple tokens', () => {
+ const Component = () => {
+ const [first, second] = useEuiI18n(
+ ['test1', 'test2'],
+ ['the first placeholder', 'the second placeholder']
+ );
+ return (
+
+ {first}
+ {second}
+
+ );
+ };
+ const component = mount( );
+ expect(component).toMatchSnapshot();
+ });
+ });
+ });
+
+ describe('mapped tokens', () => {
+ it('handles single token without values', () => {
+ const Component = () => {
+ const value = useEuiI18n('token', 'placeholder');
+ return {value}
;
+ };
+ const component = mount(
+
+
+
+ );
+ expect(component).toMatchSnapshot();
+ });
+
+ it('handles single token with values', () => {
+ const Component = () => {
+ const value = useEuiI18n('myToken', 'first {first}, then {second}', {
+ first: 'apples',
+ second: 'aardvarks',
+ });
+ return {value}
;
+ };
+ const component = mount(
+
+
+
+ );
+ expect(component).toMatchSnapshot();
+ });
+
+ it('handles multiple tokens', () => {
+ const Component = () => {
+ const [first, second] = useEuiI18n(
+ ['test1', 'test2'],
+ ['the first placeholder', 'the second placeholder']
+ );
+ return (
+
+ {first}
+ {second}
+
+ );
+ };
+ const component = mount(
+
+
+
+ );
+ expect(component).toMatchSnapshot();
+ });
+
+ describe('mappingFunc', () => {
+ it('calls the mapping function with the source string', () => {
+ const Component = () => {
+ const value = useEuiI18n('test1', 'placeholder');
+ return {value}
;
+ };
+ const component = mount(
+ value.toUpperCase(),
+ }}>
+
+
+ );
+ expect(component).toMatchSnapshot();
+ });
+ });
+ });
});
diff --git a/src/components/i18n/i18n.tsx b/src/components/i18n/i18n.tsx
index f37c541216a..f164dd8a8f0 100644
--- a/src/components/i18n/i18n.tsx
+++ b/src/components/i18n/i18n.tsx
@@ -17,10 +17,20 @@
* under the License.
*/
-import React, { Fragment, ReactChild, FunctionComponent } from 'react';
+import React, {
+ Fragment,
+ ReactChild,
+ FunctionComponent,
+ useContext,
+} from 'react';
import { EuiI18nConsumer } from '../context';
import { ExclusiveUnion } from '../common';
-import { I18nShape, Renderable, RenderableValues } from '../context/context';
+import {
+ I18nContext,
+ I18nShape,
+ Renderable,
+ RenderableValues,
+} from '../context/context';
import { processStringToChildren } from './i18n_util';
function errorOnMissingValues(token: string): never {
@@ -93,7 +103,7 @@ type EuiI18nProps<
DEFAULTS extends any[]
> = ExclusiveUnion, I18nTokensShape>;
-function hasTokens(
+function isI18nTokensShape(
x: EuiI18nProps
): x is I18nTokensShape {
return x.tokens != null;
@@ -112,7 +122,7 @@ const EuiI18n = <
{i18nConfig => {
const { mapping, mappingFunc } = i18nConfig;
- if (hasTokens(props)) {
+ if (isI18nTokensShape(props)) {
return props.children(
props.tokens.map((token, idx) =>
lookupToken(token, mapping, props.defaults[idx], mappingFunc)
@@ -136,4 +146,29 @@ const EuiI18n = <
);
-export { EuiI18n };
+function useEuiI18n<
+ T extends {},
+ DEFAULT extends Renderable,
+ DEFAULTS extends any[]
+>(token: string, defaultValue: string, values?: T): string;
+function useEuiI18n<
+ T extends {},
+ DEFAULT extends Renderable,
+ DEFAULTS extends any[]
+>(tokens: string[], defaultValues: string[]): string[];
+function useEuiI18n(...props: any[]) {
+ const i18nConfig = useContext(I18nContext);
+ const { mapping, mappingFunc } = i18nConfig;
+
+ if (typeof props[0] === 'string') {
+ const [token, defaultValue, values] = props;
+ return lookupToken(token, mapping, defaultValue, mappingFunc, values);
+ } else {
+ const [tokens, defaultValues] = props as [string[], string[]];
+ return tokens.map((token, idx) =>
+ lookupToken(token, mapping, defaultValues[idx], mappingFunc)
+ );
+ }
+}
+
+export { EuiI18n, useEuiI18n };
diff --git a/src/components/i18n/index.ts b/src/components/i18n/index.ts
index a2d16279155..8e06f382478 100644
--- a/src/components/i18n/index.ts
+++ b/src/components/i18n/index.ts
@@ -17,5 +17,5 @@
* under the License.
*/
-export { EuiI18n } from './i18n';
+export { EuiI18n, useEuiI18n } from './i18n';
export { EuiI18nNumber } from './i18n_number';
From 6757a726c45d504b5c1985f1dcabb26a29edb344 Mon Sep 17 00:00:00 2001
From: Chandler Prall
Date: Thu, 16 Jul 2020 10:57:36 -0600
Subject: [PATCH 2/7] Updated docs with useEuiI18n hook, added snippets
---
src-docs/src/views/i18n/context.js | 104 ++++++++++-----------
src-docs/src/views/i18n/i18n_attribute.js | 56 +++++++++++
src-docs/src/views/i18n/i18n_basic.js | 36 +++++--
src-docs/src/views/i18n/i18n_example.js | 80 ++++++++++++----
src-docs/src/views/i18n/i18n_multi.js | 72 ++++++++++----
src-docs/src/views/i18n/i18n_renderprop.js | 30 ------
src/components/index.js | 2 +-
7 files changed, 249 insertions(+), 131 deletions(-)
create mode 100644 src-docs/src/views/i18n/i18n_attribute.js
delete mode 100644 src-docs/src/views/i18n/i18n_renderprop.js
diff --git a/src-docs/src/views/i18n/context.js b/src-docs/src/views/i18n/context.js
index 43dd9b5597e..c62122a6c08 100644
--- a/src-docs/src/views/i18n/context.js
+++ b/src-docs/src/views/i18n/context.js
@@ -10,6 +10,7 @@ import {
EuiSpacer,
EuiI18n,
EuiI18nNumber,
+ useEuiI18n,
} from '../../../../src/components';
const mappings = {
@@ -24,6 +25,38 @@ const mappings = {
},
};
+const ContextConsumer = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {useEuiI18n('euiContext.action', 'Submit')}
+
+
+ );
+};
+
export default () => {
const [language, setLanguage] = useState('en');
@@ -33,61 +66,26 @@ export default () => {
};
return (
-
-
-
-
- setLanguage('en')}>
-
-
-
-
-
- setLanguage('fr')}>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ <>
+
+
+ setLanguage('en')}>
+
+
+
-
- {([question, action, placeholder]) => (
-
-
-
-
+
+ setLanguage('fr')}>
+
+
+
+
-
+
-
{action}
-
- )}
-
-
-
+
+
+
+ >
);
};
diff --git a/src-docs/src/views/i18n/i18n_attribute.js b/src-docs/src/views/i18n/i18n_attribute.js
new file mode 100644
index 00000000000..0fda4c845d0
--- /dev/null
+++ b/src-docs/src/views/i18n/i18n_attribute.js
@@ -0,0 +1,56 @@
+import React from 'react';
+
+import {
+ EuiCode,
+ EuiFieldText,
+ EuiI18n,
+ EuiFormRow,
+ EuiTitle,
+ useEuiI18n,
+ EuiSpacer,
+} from '../../../../src/components';
+
+export default () => {
+ return (
+ <>
+
+ useEuiI18n used in an attribute
+
+
+
+ This text field's placeholder reads from{' '}
+ euiI18nAttribute.placeholderName
+ >
+ }>
+
+
+
+
+
+
+
+ EuiI18n used as a render prop
+
+
+ {placeholderName => (
+
+ This text field's placeholder reads from{' '}
+ euiI18nAttribute.placeholderName
+ >
+ }>
+
+
+ )}
+
+ >
+ );
+};
diff --git a/src-docs/src/views/i18n/i18n_basic.js b/src-docs/src/views/i18n/i18n_basic.js
index b2d26318b7a..a488f0ca475 100644
--- a/src-docs/src/views/i18n/i18n_basic.js
+++ b/src-docs/src/views/i18n/i18n_basic.js
@@ -1,14 +1,36 @@
import React from 'react';
-import { EuiI18n } from '../../../../src/components';
+import {
+ EuiI18n,
+ EuiTitle,
+ EuiSpacer,
+ useEuiI18n,
+} from '../../../../src/components';
export default () => {
return (
-
-
-
+ <>
+
+ Basic useEuiI18n usage
+
+
+ {useEuiI18n(
+ 'euiI18nBasic.basicexample',
+ 'This is the English copy that would be replaced by a translation defined by the i18n.basicexample token.'
+ )}
+
+
+
+
+
+ Basic EuiI18n usage
+
+
+
+
+ >
);
};
diff --git a/src-docs/src/views/i18n/i18n_example.js b/src-docs/src/views/i18n/i18n_example.js
index 73961765101..34b0bcec61c 100644
--- a/src-docs/src/views/i18n/i18n_example.js
+++ b/src-docs/src/views/i18n/i18n_example.js
@@ -9,18 +9,51 @@ import { EuiCode, EuiI18n, EuiContext } from '../../../../src/components';
import I18nBasic from './i18n_basic';
const i18nBasicSource = require('!!raw-loader!./i18n_basic');
const i18nBasicHtml = renderToHtml(I18nBasic);
+const basicSnippet = [
+ `useEuiI18n('filename.token', 'default value')
+`,
+ `
+`,
+];
-import I18nRenderProp from './i18n_renderprop';
-const i18nRenderPropSource = require('!!raw-loader!./i18n_renderprop');
-const i18nRenderPropHtml = renderToHtml(I18nRenderProp);
+import I18nAttribute from './i18n_attribute';
+const i18nAttributeSource = require('!!raw-loader!./i18n_attribute');
+const i18nAttributeHtml = renderToHtml(I18nAttribute);
+const attributeSnippet = [
+ `
+`,
+ `
+ {token =>
}
+
+`,
+];
import I18nMulti from './i18n_multi';
const I18nMultiSource = require('!!raw-loader!./i18n_multi');
const I18nMultiHtml = renderToHtml(I18nMulti);
+const multiValueSnippet = [
+ `const [label, text] = useEuiI18n(
+ ['filename.label', 'filename.text'],
+ ['Default Label', 'Default Text']
+);
+
+return {text}
;
+`,
+ `
+ {([label, text]) => {text}
}
+
+`,
+];
import I18nNumber from './i18n_number';
const I18nNumberSource = require('!!raw-loader!./i18n_number');
const I18nNumberHtml = renderToHtml(I18nNumber);
+const numberSnippet = [
+ `Formatted count of users:
+`,
+];
import Context from './context';
const contextSource = require('!!raw-loader!./context');
@@ -44,37 +77,41 @@ export const I18nExample = {
],
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.
+ useEuiI18n and EuiI18n allows
+ localizing string and numeric values for internationalization. There
+ are two provided ways to use this: a React hook and a render prop
+ component. In their simplest form, these take a{' '}
+ token and a default value.{' '}
+ token provides a reference to use when mapping to a
+ localized value and default provides the
+ untranslated value when no mapping is available.
),
+ snippet: basicSnippet,
demo: ,
- props: { EuiI18n },
},
{
- title: 'As a render prop',
+ title: 'Using localized values in attributes',
source: [
{
type: GuideSectionTypes.JS,
- code: i18nRenderPropSource,
+ code: i18nAttributeSource,
},
{
type: GuideSectionTypes.HTML,
- code: i18nRenderPropHtml,
+ code: i18nAttributeHtml,
},
],
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.
+ directly to the DOM. In these cases useEuiI18n can be
+ called inline, or EuiI18n can be used as a render
+ prop child which is called with the localized value.
),
- demo: ,
+ snippet: attributeSnippet,
+ demo: ,
},
{
title: 'Multi-value lookup',
@@ -91,12 +128,14 @@ export const I18nExample = {
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 .
+ can be retrieved from the hook or via a single render prop. In this
+ case the token /default props are
+ replaced by the pluralized tokens /
+ defaults . Value injection is not supported when
+ processing more than one token.
),
+ snippet: multiValueSnippet,
demo: ,
},
{
@@ -120,6 +159,7 @@ export const I18nExample = {
render prop.
),
+ snippet: numberSnippet,
demo: ,
},
{
diff --git a/src-docs/src/views/i18n/i18n_multi.js b/src-docs/src/views/i18n/i18n_multi.js
index 0e296394c01..77402b92fac 100644
--- a/src-docs/src/views/i18n/i18n_multi.js
+++ b/src-docs/src/views/i18n/i18n_multi.js
@@ -5,29 +5,61 @@ import {
EuiSpacer,
EuiText,
EuiI18n,
+ EuiTitle,
+ useEuiI18n,
} from '../../../../src/components';
export default () => {
+ const [title, description] = useEuiI18n(
+ ['euiI18nMulti.title', 'euiI18nMulti.description'],
+ ['Card Title', 'Card Description'],
+ {}
+ );
return (
-
-
-
- Both title and description for the card are looked up in one call to{' '}
- EuiI18n
-
-
-
-
- {([title, description]) => (
-
- )}
-
-
+ <>
+
+ useEuiI18n with multiple tokens
+
+
+
+
+ Both title and description for the card are looked up in one call to{' '}
+ useEuiI18n
+
+
+
+
+
+
+
+
+
+ EuiI18n render prop with multiple tokens
+
+
+
+
+ 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_renderprop.js b/src-docs/src/views/i18n/i18n_renderprop.js
deleted file mode 100644
index 7f9dcdb2cfd..00000000000
--- a/src-docs/src/views/i18n/i18n_renderprop.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import React, { Fragment } from 'react';
-
-import {
- EuiCode,
- EuiFieldText,
- EuiI18n,
- EuiFormRow,
-} from '../../../../src/components';
-
-export default () => {
- return (
-
-
-
- {placeholderName => (
-
- This text field's placeholder reads from{' '}
- i18n.renderpropexample
- >
- }>
-
-
- )}
-
-
-
- );
-};
diff --git a/src/components/index.js b/src/components/index.js
index ef981a0b58c..affe7fa1790 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -194,7 +194,7 @@ export { EuiImage } from './image';
export { useInnerText, EuiInnerText, useRenderToText } from './inner_text';
-export { EuiI18n, EuiI18nNumber } from './i18n';
+export { EuiI18n, EuiI18nNumber, useEuiI18n } from './i18n';
export {
EuiLoadingKibana,
From 59f431565c345fae66fc1c341492914a9b2195c0 Mon Sep 17 00:00:00 2001
From: Chandler Prall
Date: Thu, 16 Jul 2020 11:43:29 -0600
Subject: [PATCH 3/7] Add support to fetch-1i8n-strings or useEuiI18n to match
EuiI18n extraction
---
scripts/babel/fetch-i18n-strings.js | 42 ++++++++-
.../i18n/__snapshots__/i18n.test.tsx.snap | 14 +++
src/components/i18n/i18n.test.tsx | 16 ++++
src/components/i18n/i18n.tsx | 4 +-
src/components/image/image.tsx | 89 +++++++++----------
5 files changed, 115 insertions(+), 50 deletions(-)
diff --git a/scripts/babel/fetch-i18n-strings.js b/scripts/babel/fetch-i18n-strings.js
index cfca578162a..37c0d92e618 100644
--- a/scripts/babel/fetch-i18n-strings.js
+++ b/scripts/babel/fetch-i18n-strings.js
@@ -18,6 +18,36 @@ function getCodeForExpression(expressionNode) {
])).code;
}
+function handleHookPath(path) {
+ const symbols = [];
+
+ const arguments = path.node.arguments;
+
+ if (arguments[0].type !== 'StringLiteral') return symbols;
+
+ const token = arguments[0].value;
+ const defStringNode = arguments[1];
+ let defString;
+ let highlighting;
+
+ if (defStringNode.type === 'StringLiteral') {
+ defString = defStringNode.value;
+ highlighting = 'string';
+ } else if (defStringNode.type === 'ArrowFunctionExpression') {
+ defString = getCodeForExpression(defStringNode);
+ highlighting = 'code';
+ }
+
+ symbols.push({
+ token,
+ defString,
+ highlighting,
+ loc: path.node.loc,
+ });
+
+ return symbols;
+}
+
function handleJSXPath(path) {
const symbols = [];
@@ -76,7 +106,17 @@ function traverseFile(filepath) {
);
}
}
- }
+ },
+ CallExpression(path) {
+ if (path.node.callee && path.node.callee.type === 'Identifier' && path.node.callee.name === 'useEuiI18n') {
+ const symbols = handleHookPath(path);
+ for (let i = 0; i < symbols.length; i++) {
+ tokenMappings.push(
+ { ...symbols[i], filepath: relative(rootDir, filepath) }
+ );
+ }
+ }
+ },
}
);
}
diff --git a/src/components/i18n/__snapshots__/i18n.test.tsx.snap b/src/components/i18n/__snapshots__/i18n.test.tsx.snap
index f099e8aa0d2..f953b842c6e 100644
--- a/src/components/i18n/__snapshots__/i18n.test.tsx.snap
+++ b/src/components/i18n/__snapshots__/i18n.test.tsx.snap
@@ -475,6 +475,20 @@ exports[`EuiI18n reading values from context rendering to dom renders a mapped s
`;
+exports[`EuiI18n useEuiI18n unmapped calls a function and renders the result to the dom 1`] = `
+
+
+
+ This is a
+ callback
+ with
+ values
+ .
+
+
+
+`;
+
exports[`EuiI18n useEuiI18n unmapped handles multiple tokens 1`] = `
diff --git a/src/components/i18n/i18n.test.tsx b/src/components/i18n/i18n.test.tsx
index 626280e32e5..a2e32f800cb 100644
--- a/src/components/i18n/i18n.test.tsx
+++ b/src/components/i18n/i18n.test.tsx
@@ -302,6 +302,22 @@ describe('EuiI18n', () => {
const component = mount( );
expect(component).toMatchSnapshot();
});
+
+ it('calls a function and renders the result to the dom', () => {
+ const values = { type: 'callback', special: 'values' };
+ const renderCallback = jest.fn(({ type, special }) => (
+
+ This is a {type} with {special}.
+
+ ));
+ const Component = () => (
+ {useEuiI18n('test', renderCallback, values)}
+ );
+ const component = mount( );
+ expect(component).toMatchSnapshot();
+
+ expect(renderCallback).toHaveBeenCalledWith(values);
+ });
});
});
diff --git a/src/components/i18n/i18n.tsx b/src/components/i18n/i18n.tsx
index f164dd8a8f0..b8127f23e24 100644
--- a/src/components/i18n/i18n.tsx
+++ b/src/components/i18n/i18n.tsx
@@ -150,12 +150,12 @@ function useEuiI18n<
T extends {},
DEFAULT extends Renderable,
DEFAULTS extends any[]
->(token: string, defaultValue: string, values?: T): string;
+>(token: string, defaultValue: DEFAULT, values?: T): string;
function useEuiI18n<
T extends {},
DEFAULT extends Renderable,
DEFAULTS extends any[]
->(tokens: string[], defaultValues: string[]): string[];
+>(tokens: string[], defaultValues: DEFAULTS): string[];
function useEuiI18n(...props: any[]) {
const i18nConfig = useContext(I18nContext);
const { mapping, mappingFunc } = i18nConfig;
diff --git a/src/components/image/image.tsx b/src/components/image/image.tsx
index 6178f175b80..fc8271f5bf3 100644
--- a/src/components/image/image.tsx
+++ b/src/components/image/image.tsx
@@ -30,7 +30,7 @@ import { EuiOverlayMask } from '../overlay_mask';
import { EuiIcon } from '../icon';
-import { EuiI18n } from '../i18n';
+import { useEuiI18n } from '../i18n';
import { EuiFocusTrap } from '../focus_trap';
@@ -164,62 +164,57 @@ export const EuiImage: FunctionComponent = ({
-
- {(closeImage: string) => (
-
-
-
-
+
+ className="euiImage__button"
+ onClick={closeFullScreen}
+ onKeyDown={onKeyDown}>
+
+
+
{optionalCaption}
);
+ const fullscreenLabel = useEuiI18n(
+ 'euiImage.openImage',
+ 'Open full screen {alt} image',
+ { alt }
+ );
if (allowFullScreen) {
return (
-
- {(openImage: string) => (
-
-
- {allowFullScreenIcon}
- {isFullScreenActive && fullScreenDisplay}
-
- )}
-
+
+
+ {allowFullScreenIcon}
+ {isFullScreenActive && fullScreenDisplay}
+
{optionalCaption}
);
From d8230bcf1788efe9ba7de2b022458ab1683f0ba8 Mon Sep 17 00:00:00 2001
From: Chandler Prall
Date: Thu, 16 Jul 2020 12:24:34 -0600
Subject: [PATCH 4/7] Fix up return types for useEuiI18n
---
src/components/i18n/i18n.tsx | 30 ++++++++++++++++++++----------
1 file changed, 20 insertions(+), 10 deletions(-)
diff --git a/src/components/i18n/i18n.tsx b/src/components/i18n/i18n.tsx
index b8127f23e24..6f705cc86d7 100644
--- a/src/components/i18n/i18n.tsx
+++ b/src/components/i18n/i18n.tsx
@@ -22,6 +22,7 @@ import React, {
ReactChild,
FunctionComponent,
useContext,
+ ReactElement,
} from 'react';
import { EuiI18nConsumer } from '../context';
import { ExclusiveUnion } from '../common';
@@ -146,16 +147,25 @@ const EuiI18n = <
);
-function useEuiI18n<
- T extends {},
- DEFAULT extends Renderable,
- DEFAULTS extends any[]
->(token: string, defaultValue: DEFAULT, values?: T): string;
-function useEuiI18n<
- T extends {},
- DEFAULT extends Renderable,
- DEFAULTS extends any[]
->(tokens: string[], defaultValues: DEFAULTS): string[];
+// A single default could be a string, react child, or render function
+type DefaultRenderType> = K extends ReactChild
+ ? K
+ : (K extends () => infer RetValue ? RetValue : never);
+
+// An array with multiple defaults can only be an array of strings or elements
+type DefaultsRenderType<
+ K extends Array
+> = K extends Array ? Item : never;
+
+function useEuiI18n>(
+ token: string,
+ defaultValue: DEFAULT,
+ values?: T
+): DefaultRenderType;
+function useEuiI18n>(
+ tokens: string[],
+ defaultValues: DEFAULTS
+): Array>;
function useEuiI18n(...props: any[]) {
const i18nConfig = useContext(I18nContext);
const { mapping, mappingFunc } = i18nConfig;
From de377fca6aed65f330957e335f05232ad5fa180b Mon Sep 17 00:00:00 2001
From: Chandler Prall
Date: Thu, 16 Jul 2020 13:26:09 -0600
Subject: [PATCH 5/7] Updated custom eslint i18n rule/package to lint
useEuiI18n usages
---
scripts/eslint-plugin/i18n.js | 198 ++++++++++++++++++++++++++++-
scripts/eslint-plugin/i18n.test.js | 110 +++++++++++++++-
2 files changed, 302 insertions(+), 6 deletions(-)
diff --git a/scripts/eslint-plugin/i18n.js b/scripts/eslint-plugin/i18n.js
index 15ed6887a1a..7d16ffc0867 100644
--- a/scripts/eslint-plugin/i18n.js
+++ b/scripts/eslint-plugin/i18n.js
@@ -15,8 +15,8 @@ function attributesArrayToLookup(attributesArray) {
}
function getDefinedValues(valuesNode) {
- if (valuesNode == null || valuesNode.expression.properties == null) return new Set();
- return valuesNode.expression.properties.reduce(
+ if (valuesNode == null || valuesNode.properties == null) return new Set();
+ return valuesNode.properties.reduce(
(valueNames, property) => {
valueNames.add(property.key.name);
return valueNames;
@@ -223,7 +223,9 @@ module.exports = {
}
// validate default string interpolation matches values
- const valueNames = getDefinedValues(attributes.values);
+ const valueNames = getDefinedValues(
+ attributes.values && attributes.values.expression
+ );
if (attributes.default.type === 'Literal') {
// default is a string literal
@@ -319,7 +321,195 @@ module.exports = {
}
// debugger;
- }
+ },
+ CallExpression(node) {
+ // Only process calls to useEuiI18n
+ if (
+ !node.callee ||
+ node.callee.type !== 'Identifier' ||
+ node.callee.name !== 'useEuiI18n'
+ )
+ return;
+
+ const arguments = node.arguments;
+
+ const isSingleToken = arguments[0].type === 'Literal';
+
+ // validate argument types
+ if (isSingleToken) {
+ // default must be either a Literal of an ArrowFunctionExpression
+ const defaultArg = arguments[1];
+ const isLiteral = defaultArg.type === 'Literal';
+ const isArrowExpression =
+ defaultArg.type === 'ArrowFunctionExpression';
+ if (!isLiteral && !isArrowExpression) {
+ context.report({
+ node,
+ loc: defaultArg.loc,
+ messageId: 'invalidDefaultType',
+ data: { type: defaultArg.type },
+ });
+ return;
+ }
+ } else {
+ const tokensArg = arguments[0];
+ const defaultsArg = arguments[1];
+
+ // tokens must be an array of Literals
+ if (tokensArg.type !== 'ArrayExpression') {
+ context.report({
+ node,
+ loc: tokensArg.loc,
+ messageId: 'invalidTokensType',
+ data: { type: tokensArg.type },
+ });
+ return;
+ }
+
+ for (let i = 0; i < tokensArg.elements.length; i++) {
+ const tokenNode = tokensArg.elements[i];
+ if (
+ tokenNode.type !== 'Literal' ||
+ typeof tokenNode.value !== 'string'
+ ) {
+ context.report({
+ node,
+ loc: tokenNode.loc,
+ messageId: 'invalidTokensType',
+ data: { type: tokenNode.type }
+ });
+ return;
+ }
+ }
+
+ // defaults must be an array of either Literals or ArrowFunctionExpressions
+ if (defaultsArg.type !== 'ArrayExpression') {
+ context.report({
+ node,
+ loc: defaultsArg.loc,
+ messageId: 'invalidDefaultsType',
+ data: { type: defaultsArg.type }
+ });
+ return;
+ }
+
+ for (let i = 0; i < defaultsArg.elements.length; i++) {
+ const defaultNode = defaultsArg.elements[i];
+ if (
+ defaultNode.type !== 'Literal' ||
+ typeof defaultNode.value !== 'string'
+ ) {
+ context.report({
+ node,
+ loc: defaultNode.loc,
+ messageId: 'invalidDefaultsType',
+ data: { type: defaultNode.type }
+ });
+ return;
+ }
+ }
+ }
+
+ if (isSingleToken) {
+ const tokenArgument = arguments[0];
+ const defaultArgument = arguments[1];
+ const valuesArgument = arguments[2];
+
+ // validate token format
+ const tokenParts = tokenArgument.value.split('.');
+ if (
+ tokenParts.length <= 1 ||
+ tokenParts[0] !== expectedTokenNamespace
+ ) {
+ context.report({
+ node,
+ loc: tokenArgument.loc,
+ messageId: 'invalidToken',
+ data: {
+ tokenValue: tokenArgument.value,
+ tokenNamespace: expectedTokenNamespace,
+ },
+ });
+ }
+
+ // validate default string interpolation matches values
+ const valueNames = getDefinedValues(valuesArgument);
+
+ if (defaultArgument.type === 'Literal') {
+ // default is a string literal
+ const expectedNames = getExpectedValueNames(defaultArgument.value);
+ if (areSetsEqual(expectedNames, valueNames) === false) {
+ context.report({
+ node,
+ loc: valuesArgument.loc,
+ messageId: 'mismatchedValues',
+ data: {
+ expected: formatSet(expectedNames),
+ provided: formatSet(valueNames),
+ },
+ });
+ }
+ } else {
+ // default is a function
+ // validate the destructured param defined by default function match the values
+ const defaultFn = defaultArgument;
+ const objProperties =
+ defaultFn.params && defaultFn.params[0]
+ ? defaultFn.params[0].properties
+ : [];
+ const expectedNames = new Set(
+ objProperties.map(property => property.key.name)
+ );
+ if (areSetsEqual(valueNames, expectedNames) === false) {
+ context.report({
+ node,
+ loc: valuesArgument.loc,
+ messageId: 'mismatchedValues',
+ data: {
+ expected: formatSet(expectedNames),
+ provided: formatSet(valueNames),
+ },
+ });
+ }
+ }
+ } else {
+ // has multiple tokens
+ const tokensArgument = arguments[0];
+ const defaultsArgument = arguments[1];
+
+ // validate their names
+ const tokens = tokensArgument.elements;
+ for (let i = 0; i < tokens.length; i++) {
+ const token = tokens[i];
+ const tokenParts = token.value.split('.');
+ if (
+ tokenParts.length <= 1 ||
+ tokenParts[0] !== expectedTokenNamespace
+ ) {
+ context.report({
+ node,
+ loc: token.loc,
+ messageId: 'invalidToken',
+ data: { tokenValue: token.value, tokenNamespace: expectedTokenNamespace }
+ });
+ }
+ }
+
+ // validate the number of tokens equals the number of defaults
+ const defaults = defaultsArgument.elements;
+ if (tokens.length !== defaults.length) {
+ context.report({
+ node,
+ loc: node.loc,
+ messageId: 'mismatchedTokensAndDefaults',
+ data: {
+ tokenLength: tokens.length,
+ defaultsLength: defaults.length,
+ },
+ });
+ }
+ }
+ },
// callback functions
};
}
diff --git a/scripts/eslint-plugin/i18n.test.js b/scripts/eslint-plugin/i18n.test.js
index 6b3045d71d9..ed3585b7c74 100644
--- a/scripts/eslint-plugin/i18n.test.js
+++ b/scripts/eslint-plugin/i18n.test.js
@@ -2,12 +2,13 @@ const rule = require('./i18n');
const RuleTester = require('eslint').RuleTester;
const ruleTester = new RuleTester({
- parser: 'babel-eslint'
+ parser: require.resolve('babel-eslint')
});
const valid = [
+ /** EuiI18n **/
// nothing to validate against
- ' ',
+ ' ',
// values agree with default string
` `,
@@ -30,8 +31,22 @@ const valid = [
// default callback params match values
` name}/>`,
+
+ /** useEuiI18n **/
+ // nothing to validate against
+ `useI18n('euiFooBar.tokenName', 'Some default value')`,
+
+ // values agree with default string
+ `useEuiI18n('euiFooBar.tokenName', '{value}, {value2}', { value: 'Hello', value2: 'World' })`,
+
+ // valid tokens
+ `useEuiI18n(['euiFooBar.token1', 'euiFooBar.token2'], ['value1', 'value 2'])`,
+
+ // default callback params match values
+ `useEuiI18n('euiFooBar.token', ({ name }) => name, { name: 'John' })`,
];
const invalid = [
+ /** EuiI18n **/
// token doesn't match file name
{
code: ' ',
@@ -157,6 +172,97 @@ const invalid = [
code: ``,
errors: [{ messageId: 'invalidDefaultsType', data: { type: 'Literal' } }]
},
+
+ // /** useEuiI18n **/
+ // token doesn't match file name
+ {
+ code: `useEuiI18n('euiFooeyBar.tokenName', 'Some default value')`,
+ errors: [{ messageId: 'invalidToken', data: { tokenValue: 'euiFooeyBar.tokenName', tokenNamespace: 'euiFooBar' } }]
+ },
+
+ // token doesn't have at least two parts
+ {
+ code: `useEuiI18n('euiFooBar', 'Some default value')`,
+ errors: [{ messageId: 'invalidToken', data: { tokenValue: 'euiFooBar', tokenNamespace: 'euiFooBar' } }]
+ },
+ {
+ code: `useEuiI18n('tokenName', 'Some default value')`,
+ errors: [{ messageId: 'invalidToken', data: { tokenValue: 'tokenName', tokenNamespace: 'euiFooBar' } }]
+ },
+
+ // invalid tokens
+ {
+ code: `useEuiI18n(['euiFooBar.token1', 'token2'], ['value1', 'value 2'])`,
+ errors: [{ messageId: 'invalidToken', data: { tokenValue: 'token2', tokenNamespace: 'euiFooBar' } }]
+ },
+ {
+ code: `useEuiI18n(['euiFooeyBar.token1', 'euiFooBar.token2'], ['value1', 'value 2'])`,
+ errors: [{ messageId: 'invalidToken', data: { tokenValue: 'euiFooeyBar.token1', tokenNamespace: 'euiFooBar' } }]
+ },
+ {
+ code: `useEuiI18n(['euiFooBar.token1'], ['value1', 'value 2'])`,
+ errors: [{ messageId: 'mismatchedTokensAndDefaults', data: { tokenLength: 1, defaultsLength: 2 } }]
+ },
+
+ // values not in agreement with default string
+ {
+ code: `useEuiI18n('euiFooBar.tokenName', '{value}, {value2}', { valuee: 'Hello', value2: 'World' })`,
+ errors: [{
+ messageId: 'mismatchedValues',
+ data: {
+ expected: 'value, value2',
+ provided: 'value2, valuee'
+ }
+ }]
+ },
+ {
+ code: `useEuiI18n('euiFooBar.tokenName', '{valuee}, {value2}', { value: 'Hello', value2: 'World' })`,
+ errors: [{
+ messageId: 'mismatchedValues',
+ data: {
+ expected: 'value2, valuee',
+ provided: 'value, value2'
+ }
+ }]
+ },
+
+ // default callback params don't match values
+ {
+ code: `useEuiI18n('euiFooBar.token', ({ name }) => name, { nare: 'John' })`,
+ errors: [{
+ messageId: 'mismatchedValues',
+ data: {
+ expected: 'name',
+ provided: 'nare'
+ }
+ }]
+ },
+
+ // invalid attribute types
+ {
+ code: `useEuiI18n('euiFooBar.token', ['value'])`,
+ errors: [{ messageId: 'invalidDefaultType', data: { type: 'ArrayExpression' } }]
+ },
+ {
+ code: `useEuiI18n(5, ['value'])`,
+ errors: [{ messageId: 'invalidDefaultType', data: { type: 'ArrayExpression' } }]
+ },
+ {
+ code: `useEuiI18n([5], ['value'])`,
+ errors: [{ messageId: 'invalidTokensType', data: { type: 'Literal' } }]
+ },
+ {
+ code: `useEuiI18n(['euiFooBar.token'], 'value')`,
+ errors: [{ messageId: 'invalidDefaultsType', data: { type: 'Literal' } }]
+ },
+ {
+ code: `useEuiI18n(['euiFooBar.token'], 5)`,
+ errors: [{ messageId: 'invalidDefaultsType', data: { type: 'Literal' } }]
+ },
+ {
+ code: `useEuiI18n(['euiFooBar.token'], [5])`,
+ errors: [{ messageId: 'invalidDefaultsType', data: { type: 'Literal' } }]
+ },
];
function withFilename(ruleset) {
From 3850fe255f28b44fb822feb0952e71441af10031 Mon Sep 17 00:00:00 2001
From: Chandler Prall
Date: Thu, 16 Jul 2020 13:33:16 -0600
Subject: [PATCH 6/7] changelog
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c44526ee6bc..ef6014adcc2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
## [`master`](https://github.com/elastic/eui/tree/master)
+- Added `useEuiI18n` hook for localization ([#3749](https://github.com/elastic/eui/pull/3749))
+
**Bug fixes**
- Fixed `EuiComboBox` always showing a scrollbar ([#3744](https://github.com/elastic/eui/pull/3744))
From 05de2fd502901661698093de96c5b2e4be997472 Mon Sep 17 00:00:00 2001
From: Chandler Prall
Date: Thu, 16 Jul 2020 15:17:42 -0600
Subject: [PATCH 7/7] Remove something I was testing with and lost where I had
placeed it.
---
src-docs/src/views/i18n/i18n_multi.js | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src-docs/src/views/i18n/i18n_multi.js b/src-docs/src/views/i18n/i18n_multi.js
index 77402b92fac..8044ef2c90b 100644
--- a/src-docs/src/views/i18n/i18n_multi.js
+++ b/src-docs/src/views/i18n/i18n_multi.js
@@ -12,8 +12,7 @@ import {
export default () => {
const [title, description] = useEuiI18n(
['euiI18nMulti.title', 'euiI18nMulti.description'],
- ['Card Title', 'Card Description'],
- {}
+ ['Card Title', 'Card Description']
);
return (
<>