UNPKG

@hi18n/react

Version:

Message internationalization meets immutability and type-safety - runtime for React

260 lines 8.2 kB
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import React from "react"; import { LocaleContext } from "@hi18n/react-context"; import { Book, getTranslator, } from "@hi18n/core"; export { LocaleContext } from "@hi18n/react-context"; /** * Renders the children with the specified locale. * * @since 0.1.0 (`@hi18n/react`) * * @example * ```tsx * ReactDOM.render( * root, * <LocaleProvider locales="ja"> * <Translate id="example/greeting" book={book} /> * </LocaleProvider> * ); * ``` */ export const LocaleProvider = (props) => { const { locales, children } = props; const concatenatedLocales = Array.isArray(locales) ? locales.join("\n") : locales; const memoizedLocales = React.useMemo(() => (concatenatedLocales === "" ? [] : concatenatedLocales.split("\n")), [concatenatedLocales]); return (_jsx(LocaleContext.Provider, { value: memoizedLocales, children: children })); }; /** * Returns the locales from the context. * * @returns A list of locales in the order of preference. * * @since 0.1.2 (`@hi18n/react`) * * @example * ```tsx * const Greeting: React.FC = () => { * const { t } = useI18n(book); * return ( * <section> * <h1>{t("example/greeting")}</h1> * { * messages.length > 0 && * <p>{t("example/messages", { count: messages.length })}</p> * } * </section> * ); * }; * ``` */ export function useLocales() { // Extending string -> string | readonly string[] // to future-proof changes in how context is propagated. // Also, removing "readonly" here to correctly type Array.isArray assertion. const localesConcat = React.useContext(LocaleContext); const locales = React.useMemo(() => Array.isArray(localesConcat) ? localesConcat : localesConcat === "" ? [] : localesConcat.split("\n"), [localesConcat]); return locales; } /** * Retrieves translation helpers, using the locale from the context. * * If the catalog is not loaded yet, it suspends the component being * rendered. This is an **experimental API** which relies on React's * undocumented API for suspension. * To avoid this behavior, * initialize the Book statically or use preloadCatalog from @hi18n/core * to ensure the catalog is loaded before using this function. * * @param book A "book" object containing translated messages * @returns An object containing functions necessary for translation * * @since 0.1.0 (`@hi18n/react`) * * @example * ```tsx * const Greeting: React.FC = () => { * const { t } = useI18n(book); * return ( * <section> * <h1>{t("example/greeting")}</h1> * { * messages.length > 0 && * <p>{t("example/messages", { count: messages.length })}</p> * } * </section> * ); * }; * ``` */ export function useI18n(book) { const locales = useLocales(); const i18n = React.useMemo(() => getTranslator(book, locales, { throwPromise: true }), [book, locales]); return i18n; } /** * Renders the translated message, possibly interleaved with the elements you provide. * * If the catalog is not loaded yet, it suspends the component being * rendered. This is an **experimental API** which relies on React's * undocumented API for suspension. * To avoid this behavior, * initialize the Book statically or use preloadCatalog from @hi18n/core * to ensure the catalog is loaded before rendering this component. * * @since 0.1.0 (`@hi18n/react`) * * @example * ```tsx * <Translate id="example/signin" book={book}> * { * // These elements are inserted into the translation. * } * <a href="" /> * <a href="" /> * </Translate> * ``` * * @example You can add a placeholder for readability. * ```tsx * <Translate id="example/signin" book={book}> * You need to <a href="">sign in</a> or <a href="">sign up</a> to continue. * </Translate> * ``` * * @example Naming the elements * ```tsx * <Translate id="example/signin" book={book}> * <a key="signin" href="" /> * <a key="signup" href="" /> * </Translate> * ``` * * @example to supply non-component parameters, you can: * ```tsx * <Translate id="example/greeting" book={book} name={name} /> * ``` * * This is almost equivalent to the following: * ```tsx * const { t } = useI18n(book); * return t("example/greeting", { name }); * ``` */ export function Translate(props) { const { book, id, children, renderInElement, ...params } = props; extractComponents(children, params, { length: 0 }); fillComponentKeys(params); const translator = useI18n(book); const interpolator = getInterpolator(); const translatedChildren = translator.translateWithComponents(id, interpolator, // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument params); if (renderInElement) { return React.cloneElement(renderInElement, {}, _jsx(_Fragment, { children: translatedChildren })); } else { return _jsx(_Fragment, { children: translatedChildren }); } } /** * A variant of {@link Translate} for dynamic translation keys * * @since 0.1.1 (`@hi18n/react`) * * @example * ```tsx * const id = translationId(book, "example/signin"); * <Translate.Dynamic id={id} book={book}> * <a href="" /> * <a href="" /> * </Translate.Dynamic> * ``` */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type Translate.Dynamic = Translate; /** * A variant of {@link Translate} for translation bootstrap. * * At runtime, it just renders a TODO text. * * @since 0.1.1 (`@hi18n/react`) * * @example * ```tsx * <Translate.Todo id="example/message-to-work-on" book={book}> * </Translate.Todo> * ``` */ Translate.Todo = function Todo(props) { return _jsxs(_Fragment, { children: ["[TODO: ", props.id, "]"] }); }; // <Translate>foo<a/> <strong>bar</strong> </Translate> => { 0: <a/>, 1: <strong/> } // <Translate><strong><em></em></strong></Translate> => { 0: <strong/>, 1: <em/> } // <Translate><a key="foo" /> <button key="bar" /></Translate> => { foo: <a/>, bar: <button/> } function extractComponents(node, params, state) { if (React.isValidElement(node)) { if (node.key != null) { params[node.key] = React.cloneElement(node, { key: node.key }); } else { params[state.length] = React.cloneElement(node, { key: state.length }); state.length++; } extractComponents(node.props.children, params, state); } else if (Array.isArray(node)) { for (const child of node) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument extractComponents(child, params, state); } } } function fillComponentKeys(params) { for (const [key, value] of Object.entries( // eslint-disable-next-line @typescript-eslint/no-empty-object-type params)) { if (!React.isValidElement(value)) continue; if (value.key == null) { params[key] = React.cloneElement(value, { key }); } } } function getInterpolator() { const keys = {}; function generateKey(key) { if (!hasOwn(keys, key)) { Object.defineProperty(keys, key, { value: 1, writable: true, configurable: true, enumerable: true, }); } const id = keys[key]++; if (id === 1 && !/\$/.test(key)) { return key; } else { return `${key}$${id}`; } } function collect(submessages) { return submessages; } function wrap(component, message) { const newKey = generateKey(`${component.key}`); return React.cloneElement(component, { key: newKey }, message); } return { collect, wrap }; } function hasOwn(o, s) { return Object.prototype.hasOwnProperty.call(o, s); } //# sourceMappingURL=index.js.map