function _preformatValues(messageFormatter, values = {}, lhlMessage) {
  const customElements = collectAllCustomElementsFromICU(messageFormatter.getAst());

  /** @type {Record<string, string | number>} */
  const formattedValues = {};

  for (const [id, element] of customElements) {
    // Throw an error if a message's value isn't provided
    if (!(id in values)) {
      throw new Error(`ICU Message "${lhlMessage}" contains a value reference ("${id}") ` +
        `that wasn't provided`);
    }

    const value = values[id];

    // Direct `{id}` replacement and non-numeric values need no formatting.
    if (element.type !== TYPE.number) {
      formattedValues[id] = value;
      continue;
    }

    if (typeof value !== 'number') {
      throw new Error(`ICU Message "${lhlMessage}" contains a numeric reference ("${id}") ` +
        'but provided value was not a number');
    }

    // Format values for known styles.
    if (element.style === 'milliseconds') {
      // Round all milliseconds to the nearest 10.
      formattedValues[id] = Math.round(value / 10) * 10;
    } else if (element.style === 'seconds' && id === 'timeInMs') {
      // Convert all seconds to the correct unit (currently only for `timeInMs`).
      formattedValues[id] = Math.round(value / 100) / 10;
    } else if (element.style === 'bytes') {
      // Replace all the bytes with KB.
      formattedValues[id] = value / 1024;
    } else {
      // For all other number styles, the value isn't changed.
      formattedValues[id] = value;
    }
  }

  // Throw an error if a value is provided but has no placeholder in the message.
  for (const valueId of Object.keys(values)) {
    if (valueId in formattedValues) continue;

    // errorCode is a special case always allowed to help LighthouseError ease-of-use.
    if (valueId === 'errorCode') {
      formattedValues.errorCode = values.errorCode;
      continue;
    }

    throw new Error(`Provided value "${valueId}" does not match any placeholder in ` +
      `ICU message "${lhlMessage}"`);
  }

  return formattedValues;
}
Returns a copy of the `values` object, with the values formatted based on how
they will be used in their icuMessage, e.g. KB or milliseconds. The original
object is unchanged.
@param {IntlMessageFormat} messageFormatter
@param {Readonly<Record<string, string | number>>} values
@param {string} lhlMessage Used for clear error logging.
@return {Record<string, string | number>}