python/moz/l10n/formats/webext/serialize.py (118 lines of code) (raw):

# Copyright Mozilla Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from collections.abc import Iterator from json import dumps from re import sub from typing import Any from ...model import ( Entry, Expression, Message, PatternMessage, Resource, VariableRef, ) def webext_serialize( resource: Resource[str] | Resource[Message], trim_comments: bool = False, ) -> Iterator[str]: """ Serialize a resource as the contents of a messages.json file. Section identifiers and multi-part message identifiers are not supported. Resource and section comments are not supported. Metadata is not supported. Yields the entire JSON result as a single string. """ def check(comment: str | None, meta: Any) -> None: if trim_comments: return if comment: raise ValueError("Resource and section comments are not supported") if meta: raise ValueError("Metadata is not supported") check(resource.comment, resource.meta) res: dict[str, Any] = {} for section in resource.sections: if section.id: raise ValueError(f"Section identifiers not supported: {section.id}") check(section.comment, section.meta) for entry in section.entries: if isinstance(entry, Entry): check(None, entry.meta) if len(entry.id) != 1: raise ValueError(f"Unsupported entry identifier: {entry.id}") name = entry.id[0] if isinstance(entry.value, str): res[name] = {"message": entry.value} if not trim_comments and entry.comment: res[name]["description"] = entry.comment elif isinstance(entry.value, PatternMessage): try: msgstr, placeholders = webext_serialize_message( entry.value, trim_comments=trim_comments ) except ValueError as err: raise ValueError(f"Error serializing {name}") from err msg: dict[str, Any] = {"message": msgstr} if not trim_comments and entry.comment: msg["description"] = entry.comment if placeholders: msg["placeholders"] = placeholders res[name] = msg else: raise ValueError(f"Unsupported entry for {name}: {entry.value}") else: check(entry.comment, None) yield dumps(res, indent=2, ensure_ascii=False) yield "\n" def webext_serialize_message( msg: Message, *, trim_comments: bool = False ) -> tuple[str, dict[str, Any]]: """ Serialize a message in its messages.json representation. Returns a tuple consisting of the `"message"` string and a `"placeholders"` object. """ if not isinstance(msg, PatternMessage): raise ValueError(f"Unsupported message: {msg}") msgstr = "" placeholders: dict[str, Any] = {} for part in msg.pattern: if isinstance(part, str): msgstr += sub(r"\$+", r"$\g<0>", part) elif ( isinstance(part, Expression) and isinstance(part.arg, VariableRef) and part.function is None ): ph_name = part.arg.name source = part.attributes.get("source", None) local = msg.declarations.get(ph_name, None) if local: local_source = local.attributes.get("source", None) if isinstance(local_source, str): content = local_source elif isinstance(local.arg, VariableRef): content = local.arg.name if not content.startswith("$"): content = f"${content}" elif isinstance(local.arg, str): content = local.arg else: raise ValueError(f"Unsupported placeholder for {ph_name}: {local}") if local.function: raise ValueError(f"Unsupported annotation for {ph_name}: {local}") if ( isinstance(source, str) and len(source) >= 3 and source.startswith("$") and source.endswith("$") ): ph_name = source[1:-1] else: source = None if not any(key.lower() == ph_name.lower() for key in placeholders): placeholders[ph_name] = {"content": content} example = ( None if trim_comments else local.attributes.get("example", None) ) if isinstance(example, str): placeholders[ph_name]["example"] = example elif example: raise ValueError( f"Unsupported placeholder example for {ph_name}: {example}" ) msgstr += source or f"${ph_name}$" else: arg_name = source if isinstance(source, str) else ph_name msgstr += arg_name if arg_name.startswith("$") else f"${arg_name}" else: raise ValueError(f"Unsupported message part: {part}") return msgstr, placeholders