All files / src/react i18n-context.ts

100% Statements 32/32
100% Branches 18/18
100% Functions 6/6
100% Lines 32/32

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95  1x 1x             1x                             1x 10x 10x   10x 10x 10x 10x 2x 7x 7x 7x 8x 10x 10x 10x 10x 10x 10x 10x               1x 13x 13x 2x 2x 11x 11x                       1x                       1x                       1x  
import { type I18n, type LocaleLang, type TFunction, type TFunctionArgs } from "@embra/i18n";
import { useValue } from "@embra/reactivity/react";
import { type FC, type PropsWithChildren, createContext, createElement, useContext, useMemo } from "react";
 
interface Ctx {
  t: TFunction;
  i18n: I18n;
}
 
const I18nContext = /* @__PURE__ */ createContext<Ctx | null>(null);
 
export interface I18nProviderProps {
  i18n: I18n;
}
 
/**
 * Provides the I18n instance to the rest of the app.
 *
 * @example
 * ```tsx
 * <I18nProvider i18n={i18n}>
 *   <App />
 * </I18nProvider>
 */
export const I18nProvider: FC<PropsWithChildren<I18nProviderProps>> = ({ i18n, children }) => {
  const currentT = useValue(i18n.t$);
  const depT = useContext(I18nContext)?.t;
 
  return createElement(I18nContext.Provider, {
    value: useMemo(
      () => ({
        t: depT
          ? (keyPath: string, args?: TFunctionArgs): string => {
              const result = currentT(keyPath, args);
              return result === keyPath ? depT(keyPath, args) : result;
            }
          : currentT,
        i18n,
      }),
      [currentT, depT],
    ),
    children,
  });
};
 
interface UseI18nCtx {
  (): Ctx;
  (optional?: false): Ctx;
  (optional?: boolean): Ctx | null;
}
 
const useI18nCtx: UseI18nCtx = (optional => {
  const ctx = useContext(I18nContext);
  if (!ctx && !optional) {
    throw new Error("I18nProvider not found");
  }
  return ctx;
}) as UseI18nCtx;
 
export interface UseTranslate {
  (): TFunction;
  (optional?: false): TFunction;
  (optional?: boolean): TFunction | undefined;
}
 
/**
 * @returns A {@link TFunction} that translates the key from the nearest {@link I18nProvider} and fallbacks outwards.
 * If no {@link I18nProvider} found, throws an error unless `optional` is true.
 */
export const useTranslate: UseTranslate = (optional => useI18nCtx(optional)?.t) as UseTranslate;
 
export interface UseI18n {
  (): I18n;
  (optional?: false): I18n;
  (optional?: boolean): I18n | undefined;
}
 
/**
 * @returns The {@link I18n} instance from the nearest {@link I18nProvider}.
 * If no {@link I18nProvider} found, throws an error unless `optional` is true.
 */
export const useI18n: UseI18n = (optional => useI18nCtx(optional)?.i18n) as UseI18n;
 
export interface UseLang {
  (): LocaleLang;
  (optional?: false): LocaleLang;
  (optional?: boolean): LocaleLang | undefined;
}
 
/**
 * @returns The {@link LocaleLang} from the nearest {@link I18nProvider}.
 * If no {@link I18nProvider} found, throws an error unless `optional` is true.
 */
export const useLang: UseLang = (optional => useValue(useI18n(optional)?.lang$)) as UseLang;