Metadata-Version: 2.5
Name: ponte-l10n
Version: 0.1.0
Summary: Tooling for translating UI strings through a bridge language
Project-URL: Homepage, https://gitlab.com/trasno/ponte
Project-URL: Repository, https://gitlab.com/trasno/ponte.git
Project-URL: Issues, https://gitlab.com/trasno/ponte/-/issues
Project-URL: Changelog, https://gitlab.com/trasno/ponte/-/blob/main/CHANGES.rst
License-Expression: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
License-File: LICENSES/CC-BY-SA-4.0.txt
License-File: LICENSES/CC0-1.0.txt
License-File: LICENSES/GPL-2.0-only.txt
License-File: LICENSES/GPL-3.0-only.txt
License-File: LICENSES/LicenseRef-KDE-Accepted-GPL.txt
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.15
Classifier: Typing :: Typed
Requires-Python: >=3.15
Requires-Dist: polib>=1.2
Description-Content-Type: text/x-rst

.. SPDX-FileCopyrightText: 2026 Adrián Chaves <adrian@chaves.gal>
.. SPDX-License-Identifier: CC-BY-SA-4.0

=====
Ponte
=====

Ponte enables a workflow where UI strings and documentation are translated into
an English-based `bridge language
<https://en.wikipedia.org/wiki/Pivot_language>`_, which is then used to
automatically generate translations into other languages.

Installation
============

.. code-block:: sh

   uv tool install ponte-l10n

The distribution is named ``ponte-l10n`` and the command is ``ponte``; it
needs Python 3.15 or newer. With ``uvx``, name the command too::

   uvx --from ponte-l10n ponte --help

Usage
=====

.. code-block:: sh

   ponte derive ponte.po gl.po [--check] [--json] [--lexicon PATH]
   ponte derive ponte/ gl/ [--check] [--json] [--lexicon PATH]
   ponte overrides ponte.po gl.po [--diff] [--lexicon PATH]
   ponte overrides ponte/ gl/ [--lexicon PATH]

The target catalog is rewritten in place; a bridge catalog is an ordinary PO
file whose language is ``ponte``. Entries the run does not change keep their
original lines, wrapping and comments byte for byte.

With two directories, ``derive`` and ``overrides`` pair the ``*.po`` files
under them by relative path (``summit/messages/**/*.po``). Files that exist on
only one side are skipped, and ``derive`` counts them as ``skipped`` in its
summary. One file together with one directory is refused.

``--check`` computes every outcome and writes neither the target catalogs nor
the bridge catalogs, which is how to count ``drifted`` and ``unsupported``
entries without touching the tree.

``--lexicon PATH`` merges a project lexicon over the built-in Galician one. The
file is TOML with ``[labels]`` and ``[verbs]`` tables of mid-sentence surfaces::

   [labels]
   view = "Vista"

   [verbs]
   view = "visualizar"

A project key replaces the built-in one, and a new key adds project
vocabulary. Only ``labels`` and ``verbs`` are overridable; nouns, articles
and agreement stay in the built-in lexicon.

A ``[files]`` table adds the file layer: one entry per target catalog, keyed
by its path relative to the target root and holding the same two tables::

   [files."summit/messages/app.po".labels]
   view = "Ver"

In directory mode the key is the paired relative path, so the example applies
to ``gl/summit/messages/app.po``. A single target file has its parent as the
root, so the key is the file name (``gl.po``). File keys replace project ones
and may add vocabulary; an entry that matches no catalog is ignored.

A ``[[messages]]`` array adds the message layer: one entry per message, keyed
by the message's own ``msgid`` and, optionally, ``msgctxt``, and holding the
same two tables::

   [[messages]]
   msgid = "Open"

   [messages.labels]
   view = "Abrir"

   [[messages]]
   msgctxt = "menu"
   msgid = "Open"

   [messages.verbs]
   open = "abrir ficheiro"

``msgid`` and ``msgctxt`` match the catalog entry exactly. An entry without
``msgctxt`` applies only to messages that have none, so the two examples above
are different messages and two entries with the same pair are an error.
Message keys replace file and project ones and may add vocabulary; an entry
that matches no catalog entry is ignored.

The layers are language default < project < file < message < human edit. Both
commands validate the file when they read it: malformed TOML, a table other
than ``labels``, ``verbs``, ``files`` or ``messages``, an unknown table inside
a file entry or an unknown key inside a message entry, a missing or non-string
``msgid``, a non-string ``msgctxt``, a non-string value, or a duplicate key or
``(msgctxt, msgid)`` pair is an error. ``--check`` uses the overrides and still
writes nothing.

Every entry is counted as ``filled``, ``updated``, ``translated`` (human text,
never touched), ``drifted`` (the bridge disagrees with its ``msgid``),
``unsupported`` (the renderer lacks a lexeme, feature, count or referent),
``orphaned`` (the bridge is gone; the translation is kept) or ``no bridge``.
Entries the tool wrote carry a ``#. ponte: <digest>`` note, which is how a later
run tells its own output from a human edit.

Plural entries use one bridge surface in ``msgstr[0]``, for example
``%1 [file|n=%1]``; the ``n`` feature binds the word's number to the
placeholder. Bridge catalogs declare ``Plural-Forms: nplurals=1; plural=0;``.
The surface must render both English source forms; entries whose singular and
plural differ structurally are reported as ``drifted``.

``ponte overrides`` is read-only. It lists entries whose target text differs
from what the bridge would render now, in catalog order; a human edit that
happens to equal the render is not listed. ``--diff`` shows a unified diff for
each entry.

``derive --json`` replaces the summary with one JSON object per line, one per
message, in catalog order. ``l11`` parses these fields, so they are fixed:

.. list-table::
   :header-rows: 1
   :widths: 1 4

   * - Field
     - Meaning
   * - ``path``
     - Target catalog. Relative to the target root when that is a directory,
       otherwise the path as given.
   * - ``key``
     - ``[msgctxt, msgid]``; ``msgctxt`` is ``null`` when the entry has none.
   * - ``outcome``
     - ``filled``, ``updated``, ``translated``, ``drifted``, ``unsupported``,
       ``orphaned`` or ``no bridge``.
   * - ``reason``
     - Why the entry is ``unsupported`` (a missing lexeme, referent or
       feature) or ``drifted`` (the bridge cannot be parsed, or does not
       render the ``msgid`` or both plural forms). Absent otherwise.
   * - ``text``
     - The target text after the run, when the entry has one: a string, or an
       array of plural forms in index order. Absent when the entry is empty.

For example::

   {"path": "summit/messages/app.po", "key": ["menu", "Open"], "outcome": "filled", "text": "Abrir"}

Support
=======

.. list-table::
   :header-rows: 1
   :widths: 2 3

   * - Feature
     - Currently supported
   * - Target languages
     - `gl <https://en.wikipedia.org/wiki/Galician_language>`_
   * - Translation formats with built-in integration
     - `po <https://en.wikipedia.org/wiki/Gettext>`_

Python API
==========

Ponte does not have a public Python API. Its modules are internal and may
change without notice.


Development
===========

.. code-block:: sh

   uv sync
   uv run tox            # pytest + pre-commit + pyrefly + twine
   uv run tox -e py      # pytest only

Ruff and pyrefly are pinned in ``.pre-commit-config.yaml``. No lock file is
tracked, so CI installs the newest dependency versions and reports drift.

Bump the version::

   uvx bump-my-version bump minor

That rewrites the version in ``pyproject.toml``, dates the ``(unreleased)``
heading in ``CHANGES.rst`` and commits and tags ``0.2.0`` (no ``v`` prefix).
Pushing the tag makes GitLab CI upload the release to PyPI.
