All files / src/react trans.ts

100% Statements 37/37
100% Branches 18/18
100% Functions 1/1
100% Lines 37/37

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 901x                                                                                             1x 9x   8x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 8x     9x   6x 6x   9x 7x 6x 6x 3x 3x 3x 6x 7x   6x 9x 2x 4x 4x 9x 4x 4x 6x 6x  
import {
  type FC,
  type PropsWithChildren,
  type ReactNode,
  isValidElement,
  useMemo,
  createElement,
  Fragment,
} from "react";
 
export interface TProps {
  /** Locale translation message */
  message: string;
}
 
/**
 * Insert React elements to the translation message.
 *
 * @example
 * ```tsx
 * <Trans message="a{{b}}c{{d}}e">
 *   <h1 data-t-slot="b">B</h1>
 *   <p data-t-slot="d">D</p>
 * </Trans>
 * ```
 *  ↓
 * ```tsx
 * <>
 *   a<h1 data-t-slot="b">B</h1>c<p data-t-slot="d">D</p>e
 * <>
 * ```
 *
 * `data-t-slot` can be ignored if there is only one placeholder.
 *
 * @example
 * ```tsx
 * <Trans message="a{{b}}c">
 *   <h1>B</h1>
 * </Trans>
 * ```
 *  ↓
 * ```tsx
 * <>
 *   a<h1>B</h1>c
 * </>
 * ```
 */
export const Trans: FC<PropsWithChildren<TProps>> = ({ message, children }) => {
  if (!message) return null;
 
  const slices = useMemo(() => {
    const slices: string[] = [];
    const matchArgs = /{{(\S+?)}}/gi;
    let slice: RegExpExecArray | null;
    let pointer = 0;
    while ((slice = matchArgs.exec(message))) {
      slices.push(message.slice(pointer, slice.index), slice[1]);
      pointer = slice.index + slice[0].length;
    }
    slices.push(message.slice(pointer));
    return slices;
  }, [message]);
 
  // no template
  if (slices.length === 1) return createElement(Fragment, null, message);
 
  const slots: Record<string, ReactNode> = {};
  let hasSlot = false;
 
  for (const child of Array.isArray(children) ? children : [children]) {
    if (isValidElement(child)) {
      const key = (child.props as any)["data-t-slot"];
      if (key) {
        slots[key] = child;
        hasSlot = true;
      }
    }
  }
 
  const copy: ReactNode[] = slices.slice();
  if (hasSlot) {
    for (let i = 1; i < copy.length; i += 2) {
      copy[i] = slots[slices[i]] || `{{${slices[i]}}}`;
    }
  } else {
    copy[1] = children || `{{${slices[1]}}}`;
  }
  return createElement(Fragment, null, copy);
};