src/Formatter.js
import baseLogger from './logger.js';
import { getCleanedCode } from './utils.js';
const parseFormatStr = (formatStr) => {
let formatName = formatStr.toLowerCase().trim();
const formatOptions = {};
if (formatStr.indexOf('(') > -1) {
const p = formatStr.split('(');
formatName = p[0].toLowerCase().trim();
const optStr = p[1].substring(0, p[1].length - 1);
// extra for currency
if (formatName === 'currency' && optStr.indexOf(':') < 0) {
if (!formatOptions.currency) formatOptions.currency = optStr.trim();
} else if (formatName === 'relativetime' && optStr.indexOf(':') < 0) {
if (!formatOptions.range) formatOptions.range = optStr.trim();
} else {
const opts = optStr.split(';');
opts.forEach((opt) => {
if (opt) {
const [key, ...rest] = opt.split(':');
const val = rest
.join(':')
.trim()
.replace(/^'+|'+$/g, ''); // trim and replace ''
const trimmedKey = key.trim();
if (!formatOptions[trimmedKey]) formatOptions[trimmedKey] = val;
if (val === 'false') formatOptions[trimmedKey] = false;
if (val === 'true') formatOptions[trimmedKey] = true;
// eslint-disable-next-line no-restricted-globals
if (!isNaN(val)) formatOptions[trimmedKey] = parseInt(val, 10);
}
});
}
}
return {
formatName,
formatOptions,
};
};
const createCachedFormatter = (fn) => {
const cache = {};
return (v, l, o) => {
let optForCache = o;
// this cache optimization will only work for keys having 1 interpolated value
if (
o &&
o.interpolationkey &&
o.formatParams &&
o.formatParams[o.interpolationkey] &&
o[o.interpolationkey]
) {
optForCache = {
...optForCache,
[o.interpolationkey]: undefined,
};
}
const key = l + JSON.stringify(optForCache);
let frm = cache[key];
if (!frm) {
frm = fn(getCleanedCode(l), o);
cache[key] = frm;
}
return frm(v);
};
};
const createNonCachedFormatter = (fn) => (v, l, o) => fn(getCleanedCode(l), o)(v);
class Formatter {
constructor(options = {}) {
this.logger = baseLogger.create('formatter');
this.options = options;
this.init(options);
}
/* eslint no-param-reassign: 0 */
init(services, options = { interpolation: {} }) {
this.formatSeparator = options.interpolation.formatSeparator || ',';
const cf = options.cacheInBuiltFormats ? createCachedFormatter : createNonCachedFormatter;
this.formats = {
number: cf((lng, opt) => {
const formatter = new Intl.NumberFormat(lng, { ...opt });
return (val) => formatter.format(val);
}),
currency: cf((lng, opt) => {
const formatter = new Intl.NumberFormat(lng, { ...opt, style: 'currency' });
return (val) => formatter.format(val);
}),
datetime: cf((lng, opt) => {
const formatter = new Intl.DateTimeFormat(lng, { ...opt });
return (val) => formatter.format(val);
}),
relativetime: cf((lng, opt) => {
const formatter = new Intl.RelativeTimeFormat(lng, { ...opt });
return (val) => formatter.format(val, opt.range || 'day');
}),
list: cf((lng, opt) => {
const formatter = new Intl.ListFormat(lng, { ...opt });
return (val) => formatter.format(val);
}),
};
}
add(name, fc) {
this.formats[name.toLowerCase().trim()] = fc;
}
addCached(name, fc) {
this.formats[name.toLowerCase().trim()] = createCachedFormatter(fc);
}
format(value, format, lng, options = {}) {
const formats = format.split(this.formatSeparator);
if (
formats.length > 1 &&
formats[0].indexOf('(') > 1 &&
formats[0].indexOf(')') < 0 &&
formats.find((f) => f.indexOf(')') > -1)
) {
const lastIndex = formats.findIndex((f) => f.indexOf(')') > -1);
formats[0] = [formats[0], ...formats.splice(1, lastIndex)].join(this.formatSeparator);
}
const result = formats.reduce((mem, f) => {
const { formatName, formatOptions } = parseFormatStr(f);
if (this.formats[formatName]) {
let formatted = mem;
try {
// options passed explicit for that formatted value
const valOptions = options?.formatParams?.[options.interpolationkey] || {};
// language
const l = valOptions.locale || valOptions.lng || options.locale || options.lng || lng;
formatted = this.formats[formatName](mem, l, {
...formatOptions,
...options,
...valOptions,
});
} catch (error) {
this.logger.warn(error);
}
return formatted;
// eslint-disable-next-line no-else-return
} else {
this.logger.warn(`there was no format function for ${formatName}`);
}
return mem;
}, value);
return result;
}
}
export default Formatter;