All files / src/react trans.ts

100% Statements 29/29
100% Branches 16/16
100% Functions 2/2
100% Lines 26/26

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                                                                                              2x 9x   8x 7x 7x   7x 7x 7x 7x   7x 7x       8x   6x 6x   6x 7x 6x 6x 3x 3x         6x 6x 2x 4x     4x   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);
};