function bakePlaceholders(messages) {
  /** @type {Record<string, LhlMessage>} */
  const bakedMessages = {};

  for (const [key, defn] of Object.entries(messages)) {
    let message = defn.message;
    const placeholders = defn.placeholders;

    if (placeholders) {
      for (const [placeholder, {content}] of Object.entries(placeholders)) {
        if (!message.includes('$' + placeholder + '$')) {
          throw Error(`Provided placeholder "${placeholder}" not found in message "${message}".`);
        }
        // Need a global replace due to plural ICU copying placeholders
        // (and therefore ICU vars) multiple times.
        const regex = new RegExp('\\$' + placeholder + '\\$', 'g');
        message = message.replace(regex, () => content);
      }
    }

    // Check that all placeholders are gone.
    if (message.match(/\$\w+\$/)) {
      throw Error(`Message "${message}" is missing placeholder(s): ${message.match(/\$\w+\$/g)}`);
    }

    bakedMessages[key] = {message};
  }

  return bakedMessages;
}
Take a series of CTC format ICU messages and converts them to LHL format by
replacing $placeholders$ with their {ICU} values. Functional opposite of
`convertMessageToCtc`. This is commonly called as the last step in
translation.

Converts this:
messages: {
 "core/audits/seo/canonical.js | explanationDifferentDomain" {
   "message": "Points to a different domain ($ICU_0$)",
   "placeholders": {
     "ICU_0": {
       "content": "{url}",
       "example": "https://example.com/"
     },
   },
 },
}

Into this:
messages: {
 "core/audits/seo/canonical.js | explanationDifferentDomain" {
   "message": "Points to a different domain ({url})",
   },
 },
}

Throws if there is a $placeholder$ in the message that has no corresponding
value in the placeholders object, or vice versa.

@param {Record<string, CtcMessage>} messages
@return {Record<string, LhlMessage>}