@hi18n/react
Version:
Message internationalization meets immutability and type-safety - runtime for React
260 lines • 8.2 kB
JavaScript
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