repo_description:  i18next: learn once - translate everywhere
repo_topics: deno ecosystem flexibility i18n internationalization javascript nodejs translation
code: import baseLogger from './logger.js';
  import { getCleanedCode } from './utils.js';
  
  const parseFormatStr = (formatStr) => {
    let formatName = formatStr.toLowerCase().trim();
    const formatOptions = {};
    if (formatStr.includes('(')) {
      const p = formatStr.split('(');
      formatName = p[0].toLowerCase().trim();
  
      const optStr = p[1].slice(0, -1);
  
      // extra for currency
      if (formatName === 'currency' && !optStr.includes(':')) {
        if (!formatOptions.currency) formatOptions.currency = optStr.trim();
      } else if (formatName === 'relativetime' && !optStr.includes(':')) {
        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;
            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);
    }
  
    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 = {}) {
      if (!format) return value;
      if (value == null) return value;
      const formats = format.split(this.formatSeparator);
      if (
        formats.length > 1 &&
        formats[0].indexOf('(') > 1 &&
        !formats[0].includes(')') &&
        formats.find((f) => f.includes(')'))
      ) {
        const lastIndex = formats.findIndex((f) => f.includes(')'));
        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;
        } else {
          this.logger.warn(`there was no format function for ${formatName}`);
        }
        return mem;
      }, value);
  
      return result;
    }
  }
  
  export default Formatter;
  