chromium/ash/webui/recorder_app_ui/resources/scripts/cra/commands/add_strings.py

# Copyright 2024 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import logging
import re
import typing
from typing import Optional
from xml.dom import minidom

from cra import cli
from cra import util


def _get_grd_name(id: str) -> str:
    return f"IDS_RECORDER_{id}"


def _add_string_to_grdp(id: str) -> bool:
    """Adds a string to grdp file.

    Returns False if the id already existed in the grd.
    """
    grd_name = _get_grd_name(id)

    grd_path = util.get_strings_dir() / "recorder_strings.grdp"
    dom = minidom.parse(str(grd_path))
    messages = dom.getElementsByTagName("grit-part")[0]

    def find_next_sibling(name: str):
        """Find the next sibling to insert the new string."""
        for message in messages.childNodes:
            if message.nodeType != minidom.Node.ELEMENT_NODE:
                continue
            name = message.getAttribute("name")
            if name >= grd_name:
                return message
        return None

    next_sibling = find_next_sibling(grd_name)
    if next_sibling is not None and next_sibling.getAttribute(
            "name") == grd_name:
        return False

    new_message = dom.createElement('message')
    new_message.setAttribute('desc', '')
    new_message.setAttribute('name', grd_name)
    new_message.appendChild(dom.createTextNode("\n  "))
    messages.insertBefore(new_message, next_sibling)
    if next_sibling is None:
        # This is the last element, so there's no indent before next element
        messages.insertBefore(dom.createTextNode("  "), new_message)
        messages.insertBefore(dom.createTextNode("\n"), next_sibling)
    else:
        messages.insertBefore(dom.createTextNode("\n  "), next_sibling)

    # The encoding='UTF-8' is for adding xml header.
    xml = dom.toxml(encoding='UTF-8').decode() + "\n"
    # Make the header looks a bit better.
    xml = xml.replace('encoding="UTF-8"?>',
                      'encoding="UTF-8"?>\n').replace('<grit-part>',
                                                      '\n\n<grit-part>')

    with open(grd_path, "w") as grd_file:
        grd_file.write(xml)
    return True


def _add_string_to_resource_h(id: str):
    cra_root = util.get_cra_root()

    resource_header_path = cra_root.parent / "resources.h"
    with open(resource_header_path, "r") as f:
        resource_header = f.read()

    def handle_replace(match: re.Match) -> str:
        content = typing.cast(str, match[1])
        pairs = re.findall(r'\{"(\w+)",\s*(\w+)\}', content)
        pairs.append((util.to_camel_case(id), _get_grd_name(id)))
        pairs.sort()
        return ''.join(f'{{"{cra_id}", {grd_id}}},\n'
                       for cra_id, grd_id in pairs)

    resource_header = re.sub(r'(?<=kLocalizedStrings\[\] = \{)(.*)(?=\};)',
                             handle_replace,
                             resource_header,
                             flags=re.DOTALL)
    with open(resource_header_path, "w") as f:
        f.write(resource_header)
    util.run(["clang-format", "-i", str(resource_header_path)], cwd=cra_root)


def _add_string_to_i18n_ts(id: str):
    cra_root = util.get_cra_root()

    i18n_ts_path = cra_root / "core/i18n.ts"
    with open(i18n_ts_path, "r") as f:
        i18n_ts = f.read()

    def handle_replace(match: re.Match) -> str:
        content = typing.cast(str, match[1])
        ids = [id.strip() for id in content.split(',')]
        ids = [id for id in ids if id]
        ids.append(f"'{util.to_camel_case(id)}'")
        ids.sort()
        return ''.join(f'  {id},\n' for id in ids)

    i18n_ts = re.sub(r'(?<=noArgStringNames = \[\n)(.*?)(?=\])',
                     handle_replace,
                     i18n_ts,
                     flags=re.DOTALL)
    with open(i18n_ts_path, "w") as f:
        f.write(i18n_ts)


def _add_string(id: str) -> Optional[str]:
    logging.debug(f"Adding {id}")

    if not re.match(r"^[A-Z0-9_]+$", id):
        return "ID should be in CAMEL_CASE"

    if id.startswith("IDS_") or id.startswith("RECORDER_"):
        return "ID should not have IDS_RECORDER_ prefix"

    if not _add_string_to_grdp(id):
        logging.info(f"{id} already exists, skipping.")
        return

    _add_string_to_resource_h(id)
    _add_string_to_i18n_ts(id)

    return


@cli.command("add-strings",
             help="add new string entries",
             description="add new string entries to recorder app")
@cli.option("ids",
            metavar="ID",
            nargs="+",
            help="ID for the strings to be added. "
            "Should be in CAPITAL_CASE without IDS_RECORDER_ prefix.")
def cmd(ids: list[str]) -> int:
    for id in ids:
        err = _add_string(id)
        if err is not None:
            logging.error(f"Error when trying to add {id}: {err}")

    print("All strings added.\n"
          "Next steps:\n"
          "  * In /chromeos/strings/recorder_strings.grdp,"
          " add description and the text to newly added entries.\n"
          "  * In core/i18n.ts, if any string use arguments ($1, $2, ...),"
          " move those ID from noArgStrings to withArgsStrings"
          " and annotate with correct argument types.")

    return 0