Source code for aeat.locales.cli

"""Developer CLI for locale catalogue audits and scaffolding.

Catalogue-wide commands delegate YAML maintenance to
:class:`locales.manager.LocaleManager`, while ``modelo`` subcommands
route schema-local TOML translations through
:class:`locales._modelo_manager.ModeloLocaleManager`. Typer arguments
that name runtime languages use :class:`core.external_constants.OutputLanguage`.
"""

from __future__ import annotations

from pathlib import Path
from typing import Annotated

import typer

from ..core.external_constants import OutputLanguage
from ..core.i18n import tr
from ._modelo_manager import (
    ModeloLocaleCoverageRecord,
    ModeloLocaleDriftKind,
    ModeloLocaleDriftRecord,
    ModeloLocaleError,
    ModeloLocaleFieldKind,
    ModeloLocaleManager,
)
from .manager import LocaleError, LocaleManager, _covered_by_namespace

app = typer.Typer(name="locales", help=tr("cli.locales.app_help"), no_args_is_help=True)
modelo_app = typer.Typer(
    name="modelo",
    help=tr("cli.locales.modelo.app_help", default="Manage modelo schema-local translations."),
    no_args_is_help=True,
)
app.add_typer(modelo_app)


def _default_manager() -> LocaleManager:
    locales_dir = Path(__file__).parent
    return LocaleManager(locales_dir.parent, locales_dir)


def _modelo_manager(registry_root: Path | None) -> ModeloLocaleManager:
    return ModeloLocaleManager(registry_root)


RegistryRootOpt = Annotated[
    Path | None,
    typer.Option(
        "--registry-root",
        help=tr("cli.locales.modelo.registry_root_help", default="AEAT registry root override."),
    ),
]


[docs] @app.command("audit") def audit() -> None: """Print codebase-to-locale drift for every locale file.""" manager = _default_manager() codebase_keys = manager.get_codebase_keys() namespace_prefixes = tuple( marker.rstrip("*").rstrip(".") for marker in manager.get_codebase_namespaces() if marker.rstrip("*").rstrip(".") ) failed = False for locale_path in sorted(manager.locales_dir.glob("*.yml")): keys = manager.get_yaml_keys(manager.load_locale(locale_path)) missing = sorted(codebase_keys - keys) extra = sorted(key for key in keys - codebase_keys if not _covered_by_namespace(key, namespace_prefixes)) if missing or extra: failed = True typer.echo( tr( "locales.cli.audit.file_drift", locale_file=locale_path.name, missing_count=len(missing), extra_count=len(extra), ), ) for key in missing: typer.echo(tr("locales.cli.audit.key_missing", key=key)) for key in extra: typer.echo(tr("locales.cli.audit.key_extra", key=key)) else: typer.echo(tr("locales.cli.audit.file_ok", locale_file=locale_path.name)) if failed: raise typer.Exit(code=1)
[docs] @app.command("scaffold") def scaffold( check: Annotated[ bool, typer.Option("--check", help=tr("cli.locales.scaffold_check_help")), ] = False, ) -> None: """Update locale files so they match concrete codebase translation keys.""" if check: audit() return _default_manager().scaffold() typer.echo(tr("locales.cli.scaffold_updated"))
[docs] @app.command("set") def set_value( locale: Annotated[ str, typer.Argument(help=tr("cli.locales.set_locale_help", default="Locale code to update.")), ], key: Annotated[ str, typer.Argument(help=tr("cli.locales.set_key_help", default="Dotted locale key to update.")), ], value: Annotated[ str, typer.Argument(help=tr("cli.locales.set_value_help", default="Replacement locale value.")), ], ) -> None: """Set one locale string leaf.""" try: path = _default_manager().set_locale_value(locale, key, value) except LocaleError as exc: raise typer.BadParameter(str(exc)) from exc typer.echo(tr("locales.cli.set.updated", locale_file=path.name, key=key))
[docs] @app.command("remove") def remove_value( locale: Annotated[ str, typer.Argument(help=tr("cli.locales.remove_locale_help", default="Locale code to update.")), ], key: Annotated[ str, typer.Argument(help=tr("cli.locales.remove_key_help", default="Dotted locale key to remove.")), ], ) -> None: """Remove one locale string leaf.""" try: path = _default_manager().remove_locale_value(locale, key) except LocaleError as exc: raise typer.BadParameter(str(exc)) from exc typer.echo(tr("locales.cli.remove.removed", locale_file=path.name, key=key))
[docs] @modelo_app.command("audit", help=tr("cli.locales.modelo.audit_help", default="Audit modelo schema translations.")) def modelo_audit( locale: Annotated[ OutputLanguage, typer.Argument(help=tr("cli.locales.modelo.locale_help", default="Locale code to audit.")), ], modelo_id: Annotated[ str, typer.Argument(help=tr("cli.locales.modelo.modelo_help", default="Modelo id to audit.")), ], revision_id: Annotated[ str, typer.Argument(help=tr("cli.locales.modelo.revision_help", default="Revision id to audit.")), ], registry_root: RegistryRootOpt = None, ) -> None: """Audit modelo schema-local translation coverage and drift.""" try: record = _modelo_manager(registry_root).coverage_record(locale, modelo_id, revision_id) except ModeloLocaleError as exc: raise typer.BadParameter(str(exc)) from exc _echo_modelo_coverage(record) for drift in record.drift: _echo_modelo_drift(drift) if not record.complete: raise typer.Exit(code=1)
[docs] @modelo_app.command( "scaffold", help=tr("cli.locales.modelo.scaffold_help", default="Scaffold modelo schema translation files."), ) def modelo_scaffold( locale: Annotated[ OutputLanguage, typer.Argument(help=tr("cli.locales.modelo.locale_help", default="Locale code to scaffold.")), ], modelo_id: Annotated[ str, typer.Argument(help=tr("cli.locales.modelo.modelo_help", default="Modelo id to scaffold.")), ], revision_id: Annotated[ str, typer.Argument(help=tr("cli.locales.modelo.revision_help", default="Revision id to scaffold.")), ], check: Annotated[ bool, typer.Option("--check", help=tr("cli.locales.modelo.scaffold_check_help", default="Check without writing.")), ] = False, registry_root: RegistryRootOpt = None, ) -> None: """Scaffold modelo schema-local translation TOML files.""" manager = _modelo_manager(registry_root) if check: try: record = manager.coverage_record(locale, modelo_id, revision_id) except ModeloLocaleError as exc: raise typer.BadParameter(str(exc)) from exc _echo_modelo_coverage(record) for drift in record.drift: _echo_modelo_drift(drift) if not record.complete: raise typer.Exit(code=1) return try: changed = manager.scaffold_revision(locale, modelo_id, revision_id) except ModeloLocaleError as exc: raise typer.BadParameter(str(exc)) from exc if changed: for path in changed: typer.echo(tr("locales.cli.modelo.scaffold.updated", path=str(path))) else: typer.echo( tr( "locales.cli.modelo.scaffold.no_changes", default="Modelo locale scaffold already aligned.", ), )
[docs] @modelo_app.command("set", help=tr("cli.locales.modelo.set_help", default="Set one modelo schema translation.")) def modelo_set_value( locale: Annotated[ OutputLanguage, typer.Argument(help=tr("cli.locales.modelo.locale_help", default="Locale code to update.")), ], modelo_id: Annotated[ str, typer.Argument(help=tr("cli.locales.modelo.modelo_help", default="Modelo id to update.")), ], revision_id: Annotated[ str, typer.Argument(help=tr("cli.locales.modelo.revision_help", default="Revision id to update.")), ], field: Annotated[ ModeloLocaleFieldKind, typer.Argument(help=tr("cli.locales.modelo.field_help", default="Translation field to update.")), ], key: Annotated[ str, typer.Argument(help=tr("cli.locales.modelo.key_help", default="Schema key to update.")), ], value: Annotated[ str, typer.Argument(help=tr("cli.locales.modelo.value_help", default="Translated value.")), ], registry_root: RegistryRootOpt = None, ) -> None: """Set one modelo schema-local translation leaf.""" try: path = _modelo_manager(registry_root).set_translation_value(locale, modelo_id, revision_id, field, key, value) except ModeloLocaleError as exc: raise typer.BadParameter(str(exc)) from exc typer.echo(tr("locales.cli.modelo.set.updated", path=str(path), field=field.value, key=key))
[docs] @modelo_app.command( "remove", help=tr("cli.locales.modelo.remove_help", default="Remove one modelo schema translation."), ) def modelo_remove_value( locale: Annotated[ OutputLanguage, typer.Argument(help=tr("cli.locales.modelo.locale_help", default="Locale code to update.")), ], modelo_id: Annotated[ str, typer.Argument(help=tr("cli.locales.modelo.modelo_help", default="Modelo id to update.")), ], revision_id: Annotated[ str, typer.Argument(help=tr("cli.locales.modelo.revision_help", default="Revision id to update.")), ], field: Annotated[ ModeloLocaleFieldKind, typer.Argument(help=tr("cli.locales.modelo.field_help", default="Translation field to remove.")), ], key: Annotated[ str, typer.Argument(help=tr("cli.locales.modelo.key_help", default="Schema key to remove.")), ], registry_root: RegistryRootOpt = None, ) -> None: """Remove one modelo schema-local translation leaf.""" try: path = _modelo_manager(registry_root).remove_translation_value(locale, modelo_id, revision_id, field, key) except ModeloLocaleError as exc: raise typer.BadParameter(str(exc)) from exc typer.echo(tr("locales.cli.modelo.remove.removed", path=str(path), field=field.value, key=key))
[docs] @modelo_app.command("coverage", help=tr("cli.locales.modelo.coverage_help", default="Print modelo schema coverage.")) def modelo_coverage( locale: Annotated[ OutputLanguage, typer.Argument(help=tr("cli.locales.modelo.locale_help", default="Locale code to inspect.")), ], modelo_id: Annotated[ str, typer.Argument(help=tr("cli.locales.modelo.modelo_help", default="Modelo id to inspect.")), ], revision_id: Annotated[ str, typer.Argument(help=tr("cli.locales.modelo.revision_help", default="Revision id to inspect.")), ], registry_root: RegistryRootOpt = None, ) -> None: """Print modelo schema-local translation coverage.""" try: record = _modelo_manager(registry_root).coverage_record(locale, modelo_id, revision_id) except ModeloLocaleError as exc: raise typer.BadParameter(str(exc)) from exc _echo_modelo_coverage(record)
def _echo_modelo_coverage(record: ModeloLocaleCoverageRecord) -> None: typer.echo( tr( "locales.cli.modelo.coverage.row", default=( "locale={locale_code} modelo={modelo} revision={revision} " "labels={label_translated}/{label_required} help={help_translated}/{help_required}" ), locale_code=record.locale.value, modelo=record.modelo_id, revision=record.revision_id, label_translated=record.label_translated, label_required=record.label_required, help_translated=record.help_translated, help_required=record.help_required, ), ) def _echo_modelo_drift(drift: ModeloLocaleDriftRecord) -> None: if drift.kind is ModeloLocaleDriftKind.STALE: typer.echo( tr( "locales.cli.modelo.audit.stale", default=( "stale locale={locale_code} modelo={modelo} revision={revision} " "scope={scope} field={field} key={key}" ), locale_code=drift.target.locale.value, modelo=drift.target.modelo_id, revision=drift.target.revision_id or "", scope=drift.target.scope.value, field=drift.field.value, key=drift.key, ), ) return typer.echo( tr( "locales.cli.modelo.audit.missing", default=( "missing locale={locale_code} modelo={modelo} revision={revision} scope={scope} field={field} key={key}" ), locale_code=drift.target.locale.value, modelo=drift.target.modelo_id, revision=drift.target.revision_id or "", scope=drift.target.scope.value, field=drift.field.value, key=drift.key, ), ) __all__ = ["app", "modelo_app"]