/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. * * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import React, { Fragment, ReactChild, FunctionComponent, useContext, ReactElement, } from 'react'; import { OuiI18nConsumer } from '../context'; import { ExclusiveUnion } from '../common'; import { I18nContext, I18nShape, Renderable, RenderableValues, } from '../context/context'; import { processStringToChildren } from './i18n_util'; function errorOnMissingValues(token: string): never { throw new Error( `I18n mapping for token "${token}" is a formatting function but no values were provided.` ); } function lookupToken< T extends RenderableValues, DEFAULT extends Renderable, RESOLVED extends ResolvedType >( token: string, i18nMapping: I18nShape['mapping'], valueDefault: DEFAULT, i18nMappingFunc?: (token: string) => string, values?: I18nTokenShape['values'] ): RESOLVED { let renderable = (i18nMapping && i18nMapping[token]) || valueDefault; if (typeof renderable === 'function') { if (values === undefined) { return errorOnMissingValues(token); } // @ts-ignore 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); } // 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') { // 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 = () => { return {children}; }; // same reasons as above, we can't promise the transforms match the default's type return React.createElement(Component, values) as RESOLVED; } type ResolvedType = T extends (...args: any[]) => any ? ReturnType : T; interface I18nTokenShape> { token: string; default: DEFAULT; children?: (x: ResolvedType) => ReactChild; values?: T; } interface I18nTokensShape { tokens: string[]; defaults: T; children: (x: Array) => ReactChild; } export type OuiI18nProps< T, DEFAULT extends Renderable, DEFAULTS extends any[] > = ExclusiveUnion, I18nTokensShape>; function isI18nTokensShape( x: OuiI18nProps ): x is I18nTokensShape { return x.tokens != null; } // Must use the generics // 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 OuiI18n = < T extends {}, DEFAULT extends Renderable, DEFAULTS extends any[] >( props: OuiI18nProps ) => ( {(i18nConfig) => { const { mapping, mappingFunc } = i18nConfig; if (isI18nTokensShape(props)) { return props.children( props.tokens.map((token, idx) => lookupToken(token, mapping, props.defaults[idx], mappingFunc) ) ); } const tokenValue = lookupToken( props.token, mapping, props.default, mappingFunc, props.values ); if (props.children) { return props.children(tokenValue); } else { return tokenValue; } }} ); // 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 useOuiI18n>( token: string, defaultValue: DEFAULT, values?: T ): DefaultRenderType; function useOuiI18n>( tokens: string[], defaultValues: DEFAULTS ): Array>; function useOuiI18n(...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 { OuiI18n, useOuiI18n };