/* 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;
}