Skip to content

Commit

Permalink
Support specific i18n value types in single-token usage (#1861)
Browse files Browse the repository at this point in the history
* Support specific i18n value types in single-token usage

* changelog
  • Loading branch information
chandlerprall authored Apr 23, 2019
1 parent deace8d commit ed5dee8
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
**Bug fixes**

- Fixed `EuiComboBox` to not pass its `inputRef` prop down to the DOM ([#1867](https://github.com/elastic/eui/pull/1867))
- Fixed type definitions around `EuiI18n`'s `default` prop to better support use cases ([#1861](https://github.com/elastic/eui/pull/1861))

## [`10.1.0`](https://github.com/elastic/eui/tree/v10.1.0)

Expand Down
2 changes: 1 addition & 1 deletion src/components/context/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type Renderable<T> = ReactChild | ((values: T) => ReactChild);

export interface I18nShape {
mapping?: {
[key: string]: Renderable<any>;
[key: string]: Renderable<object>;
};
mappingFunc?: (value: string) => string;
formatNumber?: (x: number) => string;
Expand Down
4 changes: 3 additions & 1 deletion src/components/i18n/__snapshots__/i18n.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ exports[`EuiI18n reading values from context mappingFunc calls the mapping funct
default="This is the basic string."
token="test1"
>
<div>
<div
aria-label="THIS IS THE BASIC STRING."
>
THIS IS THE BASIC STRING.
</div>
</EuiI18n>
Expand Down
4 changes: 2 additions & 2 deletions src/components/i18n/i18n.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe('EuiI18n', () => {
);
const component = mount(
<EuiI18n token="test" default={renderCallback} values={values}>
{(result: ReactChild) => `Here's something neat: ${result}`}
{(result: string) => `Here's something neat: ${result}`}
</EuiI18n>
);
expect(component).toMatchSnapshot();
Expand Down Expand Up @@ -226,7 +226,7 @@ describe('EuiI18n', () => {
mappingFunc: (value: string) => value.toUpperCase(),
}}>
<EuiI18n token="test1" default="This is the basic string.">
{(one: ReactChild) => <div>{one}</div>}
{(one: string) => <div aria-label={one}>{one}</div>}
</EuiI18n>
</EuiContext>
);
Expand Down
46 changes: 33 additions & 13 deletions src/components/i18n/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,58 @@ function throwError(): never {
throw new Error('asdf');
}

function lookupToken<T extends RenderableValues>(
function lookupToken<
T extends RenderableValues,
DEFAULT extends Renderable<T>,
RESOLVED extends ResolvedType<DEFAULT>
>(
token: string,
i18nMapping: I18nShape['mapping'],
valueDefault: Renderable<T>,
valueDefault: DEFAULT,
i18nMappingFunc?: (token: string) => string,
values?: I18nTokenShape<T>['values']
): ReactChild {
values?: I18nTokenShape<T, DEFAULT>['values']
): RESOLVED {
let renderable = (i18nMapping && i18nMapping[token]) || valueDefault;

if (typeof renderable === 'function') {
if (values === undefined) {
return throwError();
} else {
// @ts-ignore-next-line
// TypeScript complains that `DEFAULT` doesn't have a call signature
// but we verified `renderable` is a function
return renderable(values);
}
} else if (values === undefined || typeof renderable !== 'string') {
if (i18nMappingFunc && typeof valueDefault === 'string') {
renderable = i18nMappingFunc(valueDefault);
}
return renderable;
// there's a hole in the typings here as there is no guarantee that i18nMappingFunc
// returned the same type of the default value, but we need to keep that assumption
return renderable as RESOLVED;
}

const children = processStringToChildren(renderable, values, i18nMappingFunc);
if (typeof children === 'string') {
return children;
// likewise, `processStringToChildren` returns a string or ReactChild[] depending on
// the type of `values`, so we will make the assumption that the default value is correct.
return children as RESOLVED;
}

const Component: FunctionComponent<any> = () => {
return <Fragment>{children}</Fragment>;
};
return React.createElement(Component, values);

// same reasons as above, we can't promise the transforms match the default's type
return React.createElement(Component, values) as RESOLVED;
}

interface I18nTokenShape<T> {
type ResolvedType<T> = T extends (...args: any[]) => any ? ReturnType<T> : T;

interface I18nTokenShape<T, DEFAULT extends Renderable<T>> {
token: string;
default: Renderable<T>;
children?: (x: ReactChild) => ReactChild;
default: DEFAULT;
children?: (x: ResolvedType<DEFAULT>) => ReactChild;
values?: T;
}

Expand All @@ -54,16 +69,21 @@ interface I18nTokensShape {
children: (x: ReactChild[]) => ReactChild;
}

type EuiI18nProps<T> = ExclusiveUnion<I18nTokenShape<T>, I18nTokensShape>;
type EuiI18nProps<T, DEFAULT extends Renderable<T>> = ExclusiveUnion<
I18nTokenShape<T, DEFAULT>,
I18nTokensShape
>;

function hasTokens(x: EuiI18nProps<any>): x is I18nTokensShape {
function hasTokens(x: EuiI18nProps<any, any>): x is I18nTokensShape {
return x.tokens != null;
}

// Must use the generics <T extends {}>
// If instead typed with React.FunctionComponent there isn't feedback given back to the dev
// when using a `values` object with a renderer callback.
const EuiI18n = <T extends {}>(props: EuiI18nProps<T>) => (
const EuiI18n = <T extends {}, DEFAULT extends Renderable<T>>(
props: EuiI18nProps<T, DEFAULT>
) => (
<EuiI18nConsumer>
{i18nConfig => {
const { mapping, mappingFunc } = i18nConfig;
Expand Down

0 comments on commit ed5dee8

Please sign in to comment.