static localizeObject(str_, object) {
    /**
     * Execute `cb(traceEngineI18nObject)` on every i18n object, recursively. The cb return
     * value replaces traceEngineI18nObject.
     * @param {any} obj
     * @param {(traceEngineI18nObject: {i18nId: string, values?: {}}) => LH.IcuMessage} cb
     * @param {Set<object>} seen
     */
    function recursiveReplaceLocalizableStrings(obj, cb, seen) {
      if (seen.has(seen)) {
        return;
      }

      seen.add(obj);

      if (obj instanceof Map) {
        for (const [key, value] of obj) {
          if (value && typeof value === 'object' && 'i18nId' in value) {
            obj.set(key, cb(value));
          } else {
            recursiveReplaceLocalizableStrings(value, cb, seen);
          }
        }
      } else if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
        Object.keys(obj).forEach(key => {
          const value = obj[key];
          if (value && typeof value === 'object' && 'i18nId' in value) {
            obj[key] = cb(value);
          } else {
            recursiveReplaceLocalizableStrings(value, cb, seen);
          }
        });
      } else if (Array.isArray(obj)) {
        for (let i = 0; i < obj.length; i++) {
          const value = obj[i];
          if (value && typeof value === 'object' && 'i18nId' in value) {
            obj[i] = cb(value);
          } else {
            recursiveReplaceLocalizableStrings(value, cb, seen);
          }
        }
      }
    }

    // Pass `{i18nId: string, values?: {}}` through Lighthouse's i18n pipeline.
    // This is equivalent to if we directly did `str_(UIStrings.whatever, ...)`
    recursiveReplaceLocalizableStrings(object, (traceEngineI18nObject) => {
      let values = traceEngineI18nObject.values;
      if (values) {
        values = structuredClone(values);
        for (const [key, value] of Object.entries(values)) {
          if (value && typeof value === 'object' && '__i18nBytes' in value) {
            // @ts-expect-error
            values[key] = value.__i18nBytes;
            // TODO: use an actual byte formatter. Right now, this shows the exact number of bytes.
          } else if (value && typeof value === 'object' && '__i18nMillis' in value) {
            // @ts-expect-error
            values[key] = `${value.__i18nMillis} ms`;
            // TODO: use an actual time formatter.
          } else if (value && typeof value === 'object' && 'i18nId' in value) {
            // TODO: add support for str_ values to be IcuMessage.
            // @ts-expect-error
            values[key] = str_(value.i18nId, value.values).formattedDefault;
          }
        }
      }

      return str_(traceEngineI18nObject.i18nId, values);
    }, new Set());
  }
Recursively finds all DevToolsIcuMessage objects and replaces them with LH.IcuMessage.

@param {ReturnType<i18n.createIcuMessageFn>} str_
@param {object} object