react-globalize
Version:
Bringing the i18n functionality of Globalize, backed by CLDR, to React
193 lines (164 loc) • 7.32 kB
JavaScript
import Globalize from "globalize";
import React from "react";
import generator from "./generator";
function messageSetup(globalize, props, globalizePropValues) {
var defaultMessage;
var children = props.children;
var scope = props.scope;
function getDefaultMessage(children) {
if (typeof children === "string") {
return children;
} else {
throw new Error("Invalid default message type `" + typeof children + "`");
}
}
// Set path - path as props supercedes default value.
if (props.path) {
// Override generator assumption. The generator assumes the globalizePropValues[0]
// (path) and props.children to be mutually exclusive, but this isn't
// true here for messages. Because, it's possible to use props.path (for
// path) and props.children for defaultMessage, which are two different
// variables.
globalizePropValues[0] = props.path;
} else {
// Although the generator had already set globalizePropValues[0] (path) as
// props.children, here its type is checked and its value is sanitized.
defaultMessage = getDefaultMessage(children);
globalizePropValues[0] = sanitizePath(defaultMessage);
}
// Scope path.
if (scope) {
globalizePropValues[0] = scope + "/" + globalizePropValues[0];
}
// Development mode only.
if (process.env.NODE_ENV !== "production") {
var path = props.path ? props.path.split("/") : [globalizePropValues[0]];
var getMessage = function(globalize, path) {
return globalize.cldr.get(["globalize-messages/{bundle}"].concat(path));
};
var setMessage = function(globalize, path, message) {
var data = {};
function set(data, path, value) {
var i;
var node = data;
var length = path.length;
for (i = 0; i < length - 1; i++) {
if (!node[path[i]]) {
node[path[i]] = {};
}
node = node[path[i]];
}
node[path[i]] = value;
}
set(data, [globalize.cldr.attributes.bundle].concat(path), message);
Globalize.loadMessages(data);
};
if (globalize.cldr) {
if (!getMessage(globalize, path)) {
defaultMessage = defaultMessage || getDefaultMessage(children);
setMessage(globalize, path, defaultMessage);
}
}
}
}
function replaceElements(props, formatted) {
var elements = props.elements;
function _replaceElements(format, elements) {
if (typeof format !== "string") {
throw new Error(`Missing or invalid string \`${format}\` (${typeof format})`);
}
if (typeof elements !== "object") {
throw new Error("Missing or invalid elements `" + elements + "` (" + typeof elements + ")");
}
// Given [x, y, z], it returns [x, element, y, element, z].
function spreadElementsInBetweenItems(array, element) {
var getElement = typeof element === "function" ? element : function() {
return element;
};
return array.slice(1).reduce(function(ret, item, i) {
ret.push(getElement(i), item);
return ret;
}, [array[0]]);
}
function splice(sourceArray, start, deleteCount, itemsArray) {
[].splice.apply(sourceArray, [start, deleteCount].concat(itemsArray));
}
return Object.keys(elements).reduce((nodes, key) => {
const element = elements[key];
// Insert array into the correct ret position.
function replaceNode(array, i) {
splice(nodes, i, 1, array);
}
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i];
if (typeof node !== "string") {
continue; // eslint-disable-line no-continue
}
// Empty tags, e.g., `[foo/]`.
let aux = node.split(`[${key}/]`);
if (aux.length > 1) {
aux = spreadElementsInBetweenItems(aux, element);
replaceNode(aux, i);
continue; // eslint-disable-line no-continue
}
// Start-end tags, e.g., `[foo]content[/foo]`.
const regexp = new RegExp(`\\[${key}\\][\\s\\S]*?\\[\\/${key}\\]`, "g");
const regexp2 = new RegExp(`\\[${key}\\]([\\s\\S]*?)\\[\\/${key}\\]`);
aux = node.split(regexp);
if (aux.length > 1) {
const contents = node.match(regexp).map(content => content.replace(regexp2, "$1"));
aux = spreadElementsInBetweenItems(
aux,
idx => React.cloneElement(element, {}, contents[idx]),
);
replaceNode(aux, i);
}
}
return nodes;
}, [format]);
}
// Elements replacement.
if (elements) {
formatted = _replaceElements(formatted, elements);
}
return formatted;
}
function sanitizePath(pathString) {
return pathString.trim().replace(/\{/g, "(").replace(/\}/g, ")").replace(/\//g, "|").replace(/\n/g, " ").replace(/ +/g, " ").replace(/"/g, "'");
}
// Overload Globalize's `.formatMessage` to allow default message.
var globalizeMessageFormatter = Globalize.messageFormatter;
Globalize.messageFormatter = Globalize.prototype.messageFormatter = function(pathOrMessage) {
var aux = {};
var sanitizedPath = sanitizePath(pathOrMessage);
// Globalize runtime
if (!this.cldr) {
// On runtime, the only way for deciding between using sanitizedPath or
// pathOrMessage as path is by checking which formatter exists.
arguments[0] = sanitizedPath;
aux = globalizeMessageFormatter.apply(this, arguments);
arguments[0] = pathOrMessage;
return aux || globalizeMessageFormatter.apply(this, arguments);
}
var sanitizedPathExists = this.cldr.get(["globalize-messages/{bundle}", sanitizedPath]) !== undefined;
var pathExists = this.cldr.get(["globalize-messages/{bundle}", pathOrMessage]) !== undefined;
// Want to distinguish between default message and path value - just checking
// for sanitizedPath won't be enough, because sanitizedPath !== pathOrMessage
// for paths like "salutations/hi".
if (!sanitizedPathExists && !pathExists) {
aux[this.cldr.attributes.bundle] = {};
aux[this.cldr.attributes.bundle][sanitizedPath] = pathOrMessage;
Globalize.loadMessages(aux);
sanitizedPathExists = true;
}
arguments[0] = sanitizedPathExists ? sanitizedPath : pathOrMessage;
return globalizeMessageFormatter.apply(this, arguments);
};
export default generator("formatMessage", ["path", "variables"], {
beforeFormat: function(props) {
messageSetup(this.globalize, props, this.globalizePropValues);
},
afterFormat: function(props, formattedValue) {
return replaceElements(props, formattedValue);
}
});