Source: app/util/locale/messageFormatUtil.js

/* eslint-disable import/prefer-default-export */

import React from 'react';

/**
 * Utilities to get messages from a messageFormat bundle
 * @module
 * @category localization
 */

/**
 * Checks if a message in the given MessageFormat messages bundle exists
 * @param definitions A message file generated by calling (new MessageFormat()).compile()
 * @param id The id of the message to get
 * @returns {boolean} true when the message exists
 */
export function hasMessage(definitions, id) {
  const message = id.split('.').reduce((def, key) => {
    if (typeof def[key] === 'undefined') {
      return false;
    }
    return def[key];
  }, definitions);

  return typeof message === 'function';
}

/**
 * Looks up a message in the given MessageFormat messages bundle
 * @param definitions A message file generated by calling (new MessageFormat()).compile()
 * @param id The id of the message to get
 * @param params Parameters for formatting this message, if any
 * @returns {string} The formatted message
 */
export function getMessage(definitions, id, params = {}) {
  const message = id.split('.').reduce((def, key) => {
    if (typeof def[key] === 'undefined') {
      throw new ReferenceError(`No message found with id "${key}"`);
    }
    return def[key];
  }, definitions);

  if (typeof message === 'function') {
    return message(params);
  }

  throw new ReferenceError(`No message found with id "${id}"`);
}

/**
 * Pre-processes the params for the message, so that any custom (non-primitive) values passed will
 * be stored to be used later. The param will be replaced by the param syntax itself, so when
 * getting the message, they remain as variables in the output string. This is required for the
 * post-processing step where the stored custom values will be replaced.
 *
 * @param params {Object} The params passed to the getMessage function, a modified copy of this will
 * be returned.
 * @return {{updatedParams: {}, customValueMap: {}}} An object with
 * multiple return values. The updatedParams is a cloned modification of the original that must
 * be passed to the getMessage function. The others will need to be passed to the
 * `postProcessMessage` function.
 *
 * @example
 *
 *   const {
 *     updatedParams,
 *     ...preProcessValues
 *   } = preProcessMessage(params);
 *
 *   message = getMessage(this.messages, id, updatedParams);
 *   return postProcessMessage(message, preProcessValues);
 *
 **/
export function preProcessMessage(params) {
  if (!params || !Object.keys(params).length) {
    return {
      updatedParams: {},
      customValueMap: {},
    };
  }

  const customValueMap = {};
  const updatedParams = { ...params };

  if (typeof updatedParams === 'object') {
    Object.keys(updatedParams).forEach(key => {
      // when value is not a primitive, we will do something special with it
      if (
        updatedParams[key] !== null &&
        typeof updatedParams[key] !== 'undefined' &&
        typeof updatedParams[key] !== 'string' &&
        typeof updatedParams[key] !== 'number' &&
        typeof updatedParams[key] !== 'boolean'
      ) {
        // store original param passed
        customValueMap[key] = updatedParams[key];

        // replace variables with the same syntax, so we can use them in post-processing
        updatedParams[key] = `{${key}}`;
        updatedParams[`_${key}`] = `{_${key}}`;
      }
    });
  }

  return {
    updatedParams,
    customValueMap,
  };
}

/**
 * Post-processes a message after being pre-processed and retrieved from the getMessage function, so
 * that custom variable replacement can be applied. It will replace any of the variable placeholders
 * left in tact because of the pre-processing work.
 *
 * If a param value is an object (most likely JSX) it will inlined there.
 * If the param value is a function, it will be called. When the special {link}foo{_link} syntax is
 * used in the message string, the contents between the variable placeholders (foo in this case)
 * will be passed as argument to that function. You can use the pipe `|` symbol to pass multiple
 * arguments to the function: {link}url|text{_link}.
 *
 * Since this will probably only used for JSX, in this cases the return value will be a JSX span
 * instead of a string. If you are just returning a string from your custom values, you should
 * instead use a messageFormat extension.
 *
 * @param message {string} The response of the messageFormat getMessage call
 * @param customValueMap {object} A map of custom param values, result from the pre-processing step
 * @return {*} Either the original string of no custom values are passed, otherwise a JSX object
 */
export function postProcessMessage(message, { customValueMap }) {
  const customValueChildrenMap = {};

  // if we have any custom value, we need to do post-processing
  if (Object.keys(customValueMap).length) {
    // this matches {link}foobar{_link}
    // and replaced it with just {link}
    // but also stores "foobar" for later use
    // eslint-disable-next-line no-param-reassign
    message = message.replace(/{([^}]+)}([^{]+){_\1}/gi, (match, key, children) => {
      // eslint-disable-next-line no-param-reassign
      customValueChildrenMap[key] = children;
      return `{${key}}`;
    });

    // this part will construct an array of children
    // foo {var} bar {val} baz
    // will turn into
    // [foo, {var}, bar, {val}, baz]
    // where "var" and "val" can be anything, most likely custom JSX
    const children = message
      .split(/({[^}]+})/) // split on remaining vars
      .map(item => {
        // match item on variable
        const match = /{([^}]+)}/i.exec(item);
        // if match, return value from variable (can be JSX)
        if (match) {
          const paramName = match[1];
          const paramValue = customValueMap[paramName];
          // if function, execute with stored children
          if (typeof paramValue === 'function') {
            return paramValue(...(customValueChildrenMap[paramName] || '').split('|'));
          }
          return paramValue;
        }

        return item;
      });

    if (children.length > 1) {
      // TODO: Fix "Warning: Each child in an array or iterator should have a unique "key" prop."
      // return list of children within a parent span
      return <span>{children}</span>;
    }
  }

  return message;
}