UNPKG

@hi18n/react

Version:

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

1 lines 16.3 kB
{"version":3,"file":"index.mjs","names":["React","LocaleContext","getTranslator","LocaleProvider","props","locales","children","concatenatedLocales","Array","isArray","join","useLocales","localesConcat","useContext","useMemo","split","useI18n","book","i18n","throwPromise","Translate","id","renderInElement","params","extractComponents","length","fillComponentKeys","translator","interpolator","getInterpolator","translatedChildren","translateWithComponents","cloneElement","Dynamic","Todo","node","state","isValidElement","key","child","value","Object","entries","keys","generateKey","hasOwn","defineProperty","writable","configurable","enumerable","test","collect","submessages","wrap","component","message","newKey","o","s","prototype","hasOwnProperty","call"],"sources":["../src/index.tsx"],"sourcesContent":["import React from \"react\";\nimport { LocaleContext } from \"@hi18n/react-context\";\nimport {\n Book,\n VocabularyBase,\n TranslationId,\n TranslatorObject,\n MessageArguments,\n getTranslator,\n InstantiateComponentTypes,\n ComponentInterpolator,\n} from \"@hi18n/core\";\n\nexport { LocaleContext } from \"@hi18n/react-context\";\n\n/**\n * Renders the children with the specified locale.\n *\n * @since 0.1.0 (`@hi18n/react`)\n *\n * @example\n * ```tsx\n * ReactDOM.render(\n * root,\n * <LocaleProvider locales=\"ja\">\n * <Translate id=\"example/greeting\" book={book} />\n * </LocaleProvider>\n * );\n * ```\n */\nexport const LocaleProvider: React.FC<{\n children?: React.ReactNode;\n /**\n * A list of locales in the order of preference.\n */\n locales: string | string[];\n}> = (props) => {\n const { locales, children } = props;\n const concatenatedLocales = Array.isArray(locales)\n ? locales.join(\"\\n\")\n : locales;\n return (\n <LocaleContext.Provider value={concatenatedLocales}>\n {children}\n </LocaleContext.Provider>\n );\n};\n/**\n * Returns the locales from the context.\n *\n * @returns A list of locales in the order of preference.\n *\n * @since 0.1.2 (`@hi18n/react`)\n *\n * @example\n * ```tsx\n * const Greeting: React.FC = () => {\n * const { t } = useI18n(book);\n * return (\n * <section>\n * <h1>{t(\"example/greeting\")}</h1>\n * {\n * messages.length > 0 &&\n * <p>{t(\"example/messages\", { count: messages.length })}</p>\n * }\n * </section>\n * );\n * };\n * ```\n */\nexport function useLocales(): string[] {\n const localesConcat = React.useContext(LocaleContext);\n const locales = React.useMemo(\n () => (localesConcat === \"\" ? [] : localesConcat.split(\"\\n\")),\n [localesConcat]\n );\n return locales;\n}\n\n/**\n * Retrieves translation helpers, using the locale from the context.\n *\n * If the catalog is not loaded yet, it suspends the component being\n * rendered. This is an **experimental API** which relies on React's\n * undocumented API for suspension.\n * To avoid this behavior,\n * initialize the Book statically or use preloadCatalog from @hi18n/core\n * to ensure the catalog is loaded before using this function.\n *\n * @param book A \"book\" object containing translated messages\n * @returns An object containing functions necessary for translation\n *\n * @since 0.1.0 (`@hi18n/react`)\n *\n * @example\n * ```tsx\n * const Greeting: React.FC = () => {\n * const { t } = useI18n(book);\n * return (\n * <section>\n * <h1>{t(\"example/greeting\")}</h1>\n * {\n * messages.length > 0 &&\n * <p>{t(\"example/messages\", { count: messages.length })}</p>\n * }\n * </section>\n * );\n * };\n * ```\n */\nexport function useI18n<M extends VocabularyBase>(\n book: Book<M>\n): TranslatorObject<M> {\n const locales = useLocales();\n const i18n = React.useMemo(\n () => getTranslator(book, locales, { throwPromise: true }),\n [book, locales]\n );\n return i18n;\n}\n\nexport type BaseTranslateProps<\n Vocabulary extends VocabularyBase,\n // TODO: restrict to string\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n K extends keyof any\n> = {\n /**\n * The book to look up in for the translation.\n *\n * @since 0.1.0 (`@hi18n/react`)\n */\n book: Book<Vocabulary>;\n /**\n * The translation id.\n *\n * @since 0.1.0 (`@hi18n/react`)\n */\n id: K;\n /**\n * The children. hi18n searches for elements in the node and names each one in the following way:\n *\n * - If it has a `key` prop, use the value.\n * - Otherwise, give it a number in the order of occurrence of the opening tags starting with 0.\n *\n * They are merged into the props as the parameters for the translation.\n *\n * @since 0.1.0 (`@hi18n/react`)\n */\n children?: React.ReactNode | undefined;\n /**\n * When given, the results are wrapped in the element given.\n *\n * Note that you don't need to use the prop in most cases.\n * You can just wrap the `<Translate>` element in whatever wrapper components.\n *\n * One valid use case would be to pass a component that analyzes the texts or elements within the component,\n * such as one that splits texts using `Intl.Segmenter` for better word-wrapping experience.\n *\n * @since 0.1.2 (`@hi18n/react`)\n *\n * @example\n * ```tsx\n * <Translate book={book} id=\"example/greeting\" renderInElement={<TextWrapper />}>\n * </Translate>\n * ```\n */\n renderInElement?: React.ReactElement | undefined;\n};\n\nexport type TranslateProps<\n M extends VocabularyBase,\n // TODO: restrict to string\n K extends keyof M\n> = BaseTranslateProps<M, K> &\n PartialForComponents<MessageArguments<M[K], React.ReactElement>>;\n\ntype PartialForComponents<T> = Partial<T> & Omit<T, ComponentKeys<T>>;\ntype ComponentKeys<T, K extends keyof T = keyof T> = K extends unknown\n ? T[K] extends React.ReactElement\n ? K\n : never\n : never;\n\n/**\n * Renders the translated message, possibly interleaved with the elements you provide.\n *\n * If the catalog is not loaded yet, it suspends the component being\n * rendered. This is an **experimental API** which relies on React's\n * undocumented API for suspension.\n * To avoid this behavior,\n * initialize the Book statically or use preloadCatalog from @hi18n/core\n * to ensure the catalog is loaded before rendering this component.\n *\n * @since 0.1.0 (`@hi18n/react`)\n *\n * @example\n * ```tsx\n * <Translate id=\"example/signin\" book={book}>\n * {\n * // These elements are inserted into the translation.\n * }\n * <a href=\"\" />\n * <a href=\"\" />\n * </Translate>\n * ```\n *\n * @example You can add a placeholder for readability.\n * ```tsx\n * <Translate id=\"example/signin\" book={book}>\n * You need to <a href=\"\">sign in</a> or <a href=\"\">sign up</a> to continue.\n * </Translate>\n * ```\n *\n * @example Naming the elements\n * ```tsx\n * <Translate id=\"example/signin\" book={book}>\n * <a key=\"signin\" href=\"\" />\n * <a key=\"signup\" href=\"\" />\n * </Translate>\n * ```\n *\n * @example to supply non-component parameters, you can:\n * ```tsx\n * <Translate id=\"example/greeting\" book={book} name={name} />\n * ```\n *\n * This is almost equivalent to the following:\n * ```tsx\n * const { t } = useI18n(book);\n * return t(\"example/greeting\", { name });\n * ```\n */\nexport function Translate<M extends VocabularyBase, K extends string & keyof M>(\n props: TranslateProps<M, K>\n): React.ReactElement | null {\n const { book, id, children, renderInElement, ...params } = props;\n extractComponents(children, params, { length: 0 });\n fillComponentKeys(params);\n const translator = useI18n(book);\n const interpolator = getInterpolator();\n const translatedChildren = translator.translateWithComponents<\n React.ReactNode,\n React.ReactElement,\n K\n >(\n id,\n interpolator,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n params as any\n );\n if (renderInElement) {\n return React.cloneElement(renderInElement, {}, <>{translatedChildren}</>);\n } else {\n return <>{translatedChildren}</>;\n }\n}\n\nexport type DynamicTranslateProps<\n Vocabulary extends VocabularyBase,\n Args\n> = BaseTranslateProps<Vocabulary, TranslationId<Vocabulary, Args>> &\n PartialForComponents<InstantiateComponentTypes<Args, React.ReactElement>>;\n\n/**\n * A variant of {@link Translate} for dynamic translation keys\n *\n * @since 0.1.1 (`@hi18n/react`)\n *\n * @example\n * ```tsx\n * const id = translationId(book, \"example/signin\");\n * <Translate.Dynamic id={id} book={book}>\n * <a href=\"\" />\n * <a href=\"\" />\n * </Translate.Dynamic>\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/ban-types\nTranslate.Dynamic = Translate as <Vocabulary extends VocabularyBase, Args = {}>(\n props: DynamicTranslateProps<Vocabulary, Args>\n) => React.ReactElement | null;\n\nexport type TodoTranslateProps<Vocabulary extends VocabularyBase> =\n BaseTranslateProps<Vocabulary, string> & {\n [key: string]: unknown;\n };\n\n/**\n * A variant of {@link Translate} for translation bootstrap.\n *\n * At runtime, it just renders a TODO text.\n *\n * @since 0.1.1 (`@hi18n/react`)\n *\n * @example\n * ```tsx\n * <Translate.Todo id=\"example/message-to-work-on\" book={book}>\n * </Translate.Todo>\n * ```\n */\nTranslate.Todo = function Todo<Vocabulary extends VocabularyBase>(\n props: TodoTranslateProps<Vocabulary>\n): React.ReactElement | null {\n return <>[TODO: {props.id}]</>;\n};\n\n// <Translate>foo<a/> <strong>bar</strong> </Translate> => { 0: <a/>, 1: <strong/> }\n// <Translate><strong><em></em></strong></Translate> => { 0: <strong/>, 1: <em/> }\n// <Translate><a key=\"foo\" /> <button key=\"bar\" /></Translate> => { foo: <a/>, bar: <button/> }\nfunction extractComponents(\n node: React.ReactNode,\n params: Record<string | number, unknown>,\n state: { length: number }\n) {\n if (React.isValidElement(node)) {\n if (node.key != null) {\n params[node.key] = React.cloneElement(node, { key: node.key });\n } else {\n params[state.length] = React.cloneElement(node, { key: state.length });\n state.length++;\n }\n extractComponents(node.props.children, params, state);\n } else if (Array.isArray(node)) {\n for (const child of node) {\n extractComponents(child, params, state);\n }\n }\n}\n\nfunction fillComponentKeys(params: Record<string | number, unknown>) {\n for (const [key, value] of Object.entries(\n // eslint-disable-next-line @typescript-eslint/ban-types\n params as Record<string | number, {} | null | undefined>\n )) {\n if (!React.isValidElement(value)) continue;\n if (value.key == null) {\n params[key] = React.cloneElement(value, { key });\n }\n }\n}\n\nfunction getInterpolator(): ComponentInterpolator<\n React.ReactNode,\n React.ReactElement\n> {\n const keys: Record<string, number> = {};\n\n function generateKey(key: string): string {\n if (!hasOwn(keys, key)) {\n Object.defineProperty(keys, key, {\n value: 1,\n writable: true,\n configurable: true,\n enumerable: true,\n });\n }\n const id = keys[key]++;\n if (id === 1 && !/\\$/.test(key)) {\n return key;\n } else {\n return `${key}$${id}`;\n }\n }\n\n function collect(submessages: React.ReactNode[]): React.ReactNode {\n return submessages;\n }\n\n function wrap(\n component: React.ReactElement,\n message: React.ReactNode\n ): React.ReactNode {\n const newKey = generateKey(`${component.key}`);\n return React.cloneElement(component, { key: newKey }, message);\n }\n\n return { collect, wrap };\n}\n\nfunction hasOwn(o: object, s: PropertyKey): boolean {\n return Object.prototype.hasOwnProperty.call(o, s);\n}\n"],"mappings":"AAAA,OAAOA,KAAP,MAAkB,OAAlB;AACA,SAASC,aAAT,QAA8B,sBAA9B;AACA,SAMEC,aANF,QASO,aATP;AAWA,SAASD,aAAT,QAA8B,sBAA9B;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AACA,OAAO,MAAME,cAMX,GAAIC,KAAD,IAAW;EACd,MAAM;IAAEC,OAAF;IAAWC;EAAX,IAAwBF,KAA9B;EACA,MAAMG,mBAAmB,GAAGC,KAAK,CAACC,OAAN,CAAcJ,OAAd,IACxBA,OAAO,CAACK,IAAR,CAAa,IAAb,CADwB,GAExBL,OAFJ;EAGA,oBACE,oBAAC,aAAD,CAAe,QAAf;IAAwB,KAAK,EAAEE;EAA/B,GACGD,QADH,CADF;AAKD,CAhBM;AAiBP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AACA,OAAO,SAASK,UAAT,GAAgC;EACrC,MAAMC,aAAa,GAAGZ,KAAK,CAACa,UAAN,CAAiBZ,aAAjB,CAAtB;EACA,MAAMI,OAAO,GAAGL,KAAK,CAACc,OAAN,CACd,MAAOF,aAAa,KAAK,EAAlB,GAAuB,EAAvB,GAA4BA,aAAa,CAACG,KAAd,CAAoB,IAApB,CADrB,EAEd,CAACH,aAAD,CAFc,CAAhB;EAIA,OAAOP,OAAP;AACD;AAED;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AACA,OAAO,SAASW,OAAT,CACLC,IADK,EAEgB;EACrB,MAAMZ,OAAO,GAAGM,UAAU,EAA1B;EACA,MAAMO,IAAI,GAAGlB,KAAK,CAACc,OAAN,CACX,MAAMZ,aAAa,CAACe,IAAD,EAAOZ,OAAP,EAAgB;IAAEc,YAAY,EAAE;EAAhB,CAAhB,CADR,EAEX,CAACF,IAAD,EAAOZ,OAAP,CAFW,CAAb;EAIA,OAAOa,IAAP;AACD;;AAiED;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASE,SAAT,CACLhB,KADK,EAEsB;EAC3B,MAAM;IAAEa,IAAF;IAAQI,EAAR;IAAYf,QAAZ;IAAsBgB,eAAtB;IAAuC,GAAGC;EAA1C,IAAqDnB,KAA3D;EACAoB,iBAAiB,CAAClB,QAAD,EAAWiB,MAAX,EAAmB;IAAEE,MAAM,EAAE;EAAV,CAAnB,CAAjB;EACAC,iBAAiB,CAACH,MAAD,CAAjB;EACA,MAAMI,UAAU,GAAGX,OAAO,CAACC,IAAD,CAA1B;EACA,MAAMW,YAAY,GAAGC,eAAe,EAApC;EACA,MAAMC,kBAAkB,GAAGH,UAAU,CAACI,uBAAX,CAKzBV,EALyB,EAMzBO,YANyB,EAOzB;EACAL,MARyB,CAA3B;;EAUA,IAAID,eAAJ,EAAqB;IACnB,oBAAOtB,KAAK,CAACgC,YAAN,CAAmBV,eAAnB,EAAoC,EAApC,eAAwC,0CAAGQ,kBAAH,CAAxC,CAAP;EACD,CAFD,MAEO;IACL,oBAAO,0CAAGA,kBAAH,CAAP;EACD;AACF;;AAQD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACAV,SAAS,CAACa,OAAV,GAAoBb,SAApB;;AASA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACAA,SAAS,CAACc,IAAV,GAAiB,SAASA,IAAT,CACf9B,KADe,EAEY;EAC3B,oBAAO,qDAAUA,KAAK,CAACiB,EAAhB,MAAP;AACD,CAJD,C,CAMA;AACA;AACA;;;AACA,SAASG,iBAAT,CACEW,IADF,EAEEZ,MAFF,EAGEa,KAHF,EAIE;EACA,kBAAIpC,KAAK,CAACqC,cAAN,CAAqBF,IAArB,CAAJ,EAAgC;IAC9B,IAAIA,IAAI,CAACG,GAAL,IAAY,IAAhB,EAAsB;MACpBf,MAAM,CAACY,IAAI,CAACG,GAAN,CAAN,gBAAmBtC,KAAK,CAACgC,YAAN,CAAmBG,IAAnB,EAAyB;QAAEG,GAAG,EAAEH,IAAI,CAACG;MAAZ,CAAzB,CAAnB;IACD,CAFD,MAEO;MACLf,MAAM,CAACa,KAAK,CAACX,MAAP,CAAN,gBAAuBzB,KAAK,CAACgC,YAAN,CAAmBG,IAAnB,EAAyB;QAAEG,GAAG,EAAEF,KAAK,CAACX;MAAb,CAAzB,CAAvB;MACAW,KAAK,CAACX,MAAN;IACD;;IACDD,iBAAiB,CAACW,IAAI,CAAC/B,KAAL,CAAWE,QAAZ,EAAsBiB,MAAtB,EAA8Ba,KAA9B,CAAjB;EACD,CARD,MAQO,IAAI5B,KAAK,CAACC,OAAN,CAAc0B,IAAd,CAAJ,EAAyB;IAC9B,KAAK,MAAMI,KAAX,IAAoBJ,IAApB,EAA0B;MACxBX,iBAAiB,CAACe,KAAD,EAAQhB,MAAR,EAAgBa,KAAhB,CAAjB;IACD;EACF;AACF;;AAED,SAASV,iBAAT,CAA2BH,MAA3B,EAAqE;EACnE,KAAK,MAAM,CAACe,GAAD,EAAME,KAAN,CAAX,IAA2BC,MAAM,CAACC,OAAP,EACzB;EACAnB,MAFyB,CAA3B,EAGG;IACD,IAAI,eAACvB,KAAK,CAACqC,cAAN,CAAqBG,KAArB,CAAL,EAAkC;;IAClC,IAAIA,KAAK,CAACF,GAAN,IAAa,IAAjB,EAAuB;MACrBf,MAAM,CAACe,GAAD,CAAN,gBAActC,KAAK,CAACgC,YAAN,CAAmBQ,KAAnB,EAA0B;QAAEF;MAAF,CAA1B,CAAd;IACD;EACF;AACF;;AAED,SAAST,eAAT,GAGE;EACA,MAAMc,IAA4B,GAAG,EAArC;;EAEA,SAASC,WAAT,CAAqBN,GAArB,EAA0C;IACxC,IAAI,CAACO,MAAM,CAACF,IAAD,EAAOL,GAAP,CAAX,EAAwB;MACtBG,MAAM,CAACK,cAAP,CAAsBH,IAAtB,EAA4BL,GAA5B,EAAiC;QAC/BE,KAAK,EAAE,CADwB;QAE/BO,QAAQ,EAAE,IAFqB;QAG/BC,YAAY,EAAE,IAHiB;QAI/BC,UAAU,EAAE;MAJmB,CAAjC;IAMD;;IACD,MAAM5B,EAAE,GAAGsB,IAAI,CAACL,GAAD,CAAJ,EAAX;;IACA,IAAIjB,EAAE,KAAK,CAAP,IAAY,CAAC,KAAK6B,IAAL,CAAUZ,GAAV,CAAjB,EAAiC;MAC/B,OAAOA,GAAP;IACD,CAFD,MAEO;MACL,iBAAUA,GAAV,cAAiBjB,EAAjB;IACD;EACF;;EAED,SAAS8B,OAAT,CAAiBC,WAAjB,EAAkE;IAChE,OAAOA,WAAP;EACD;;EAED,SAASC,IAAT,CACEC,SADF,EAEEC,OAFF,EAGmB;IACjB,MAAMC,MAAM,GAAGZ,WAAW,WAAIU,SAAS,CAAChB,GAAd,EAA1B;IACA,oBAAOtC,KAAK,CAACgC,YAAN,CAAmBsB,SAAnB,EAA8B;MAAEhB,GAAG,EAAEkB;IAAP,CAA9B,EAA+CD,OAA/C,CAAP;EACD;;EAED,OAAO;IAAEJ,OAAF;IAAWE;EAAX,CAAP;AACD;;AAED,SAASR,MAAT,CAAgBY,CAAhB,EAA2BC,CAA3B,EAAoD;EAClD,OAAOjB,MAAM,CAACkB,SAAP,CAAiBC,cAAjB,CAAgCC,IAAhC,CAAqCJ,CAArC,EAAwCC,CAAxC,CAAP;AACD"}