> ## Documentation Index
> Fetch the complete documentation index at: https://hyperlocalise.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Gettext POT and PO

> Extract backend messages to a POT template, maintain PO catalogs, and review translations in Cloud.

This tutorial connects a Gettext-based backend to Hyperlocalise Cloud. Your extraction tool remains responsible for code scanning and the `.pot` template. Hyperlocalise syncs the resulting `.po` catalogs for translation and review.

<Warning>
  The current Hyperlocalise PO parser is best suited to singular, context-free messages. It keys entries by `msgid`, ignores `msgctxt`, and writes only `msgstr` or `msgstr[0]`. Do not use this workflow for catalogs that depend on context-disambiguated duplicate `msgid` values or full Gettext plural arrays.
</Warning>

## Prerequisites

You need:

* a backend that uses GNU Gettext-compatible catalogs;
* `xgettext`, `msginit`, and `msgmerge`;
* the [Hyperlocalise CLI](/cli/getting-started/install);
* a Cloud project with `en-US` as source and `fr-FR` and `de-DE` as targets; and
* `HYPERLOCALISE_API_KEY` and `HYPERLOCALISE_PROJECT_ID` in your environment.

The example uses:

```text theme={null}
.
├── src/
├── locale/
│   ├── messages.pot
│   ├── en-US/LC_MESSAGES/messages.po
│   ├── fr-FR/LC_MESSAGES/messages.po
│   └── de-DE/LC_MESSAGES/messages.po
└── i18n.yml
```

## 1. Mark backend messages

The exact API depends on your language. A Python service might use:

```python theme={null}
from gettext import gettext as _

def saved_filter_message(name: str) -> str:
    return _("Saved filter: {name}").format(name=name)
```

Keep placeholders named and stable. Hyperlocalise can compare `{name}` between source and target.

Add translator comments when your extraction tool supports them:

```python theme={null}
# Translators: confirmation shown after a user saves a filter.
message = _("Saved filter: {name}")
```

## 2. Extract a POT template

Run the command appropriate for your backend. For Python:

```bash theme={null}
xgettext \
  --language=Python \
  --keyword=_ \
  --add-comments=Translators: \
  --from-code=UTF-8 \
  --output=locale/messages.pot \
  src/**/*.py
```

The POT file is the extraction template:

```po theme={null}
#. Translators: confirmation shown after a user saves a filter.
#: src/filters.py:4
msgid "Saved filter: {name}"
msgstr ""
```

Commit `messages.pot`. It records source changes, but do not map it in `i18n.yml`: Hyperlocalise currently selects the PO parser for `.po`, not `.pot`.

## 3. Create and update the source PO catalog

Create the English catalog once:

```bash theme={null}
mkdir -p locale/en-US/LC_MESSAGES
msginit \
  --input=locale/messages.pot \
  --output-file=locale/en-US/LC_MESSAGES/messages.po \
  --locale=en_US.UTF-8 \
  --no-translator
```

When code changes, update it:

```bash theme={null}
msgmerge \
  --update \
  --backup=none \
  locale/en-US/LC_MESSAGES/messages.po \
  locale/messages.pot
```

Ensure each English `msgstr` contains the source text. Hyperlocalise reads `msgid` as the stable key and `msgstr` as the source value. Empty source `msgstr` values do not provide content to translate.

## 4. Map PO catalogs

Create `i18n.yml`:

```yaml theme={null}
locales:
  source: en-US
  targets:
    - fr-FR
    - de-DE

buckets:
  backend:
    files:
      - from: locale/{{source}}/LC_MESSAGES/messages.po
        to: locale/{{target}}/LC_MESSAGES/messages.po

hyperlocalise:
  project_id_env: HYPERLOCALISE_PROJECT_ID
  api_base_url: https://hyperlocalise.com/api
  api_key_env: HYPERLOCALISE_API_KEY
```

Keep locale selection in the directory structure. After pull, check the PO header expected by your runtime; reconstruction uses the source PO as the structural template.

## 5. Check extraction drift

In the source pull request, regenerate the template and update the source PO:

```bash theme={null}
./scripts/extract-messages.sh
git diff --exit-code -- locale/messages.pot locale/en-US/LC_MESSAGES/messages.po
hl sync push --dry-run
```

Put the exact `xgettext` and `msgmerge` commands in `scripts/extract-messages.sh` so local and CI extraction agree.

This gate catches messages added in code without an updated catalog.

## 6. Push and review messages

After merge:

```bash theme={null}
hl sync push
```

Start translation through a **Source upload** automation or **New Request**. Review:

* placeholders such as `{name}` or `%s`;
* translator comments and source references;
* punctuation produced by the surrounding code;
* terminology shared with API errors and emails; and
* strings that may reach logs instead of end users.

Do not send internal diagnostics, secrets, or user data for translation. Approve the target catalogs after review.

## 7. Pull and validate PO files

Run:

```bash theme={null}
hl sync pull
hl check --bucket backend --quiet
msgfmt --check --check-format \
  --output-file=/dev/null \
  locale/fr-FR/LC_MESSAGES/messages.po
msgfmt --check --check-format \
  --output-file=/dev/null \
  locale/de-DE/LC_MESSAGES/messages.po
```

`msgfmt` validates PO syntax and format strings. Inspect each target header after pull and set runtime-specific `Language`, encoding, or domain metadata when your framework requires it.

Compile catalogs as part of the build:

```bash theme={null}
msgfmt \
  locale/fr-FR/LC_MESSAGES/messages.po \
  --output-file=locale/fr-FR/LC_MESSAGES/messages.mo
```

Generate `.mo` files during build or release. Avoid committing them unless your deployment process requires compiled catalogs in git.

## 8. Test the backend

Open a translation pull request and test:

* locale negotiation selects the expected catalog;
* placeholders interpolate without exceptions;
* missing translations follow your intended fallback;
* API responses preserve machine-readable error codes; and
* translated emails or HTML remain escaped correctly.

Keep protocol identifiers and error codes outside translatable `msgid` values.

## Troubleshooting

### `sync push` ignores `messages.pot`

That is expected. Generate a source `.po` file from the POT template and map the `.po` file in `i18n.yml`.

### Context variants overwrite each other

The current parser ignores `msgctxt`. Give messages distinct `msgid` values or keep that catalog outside this workflow.

### Only one plural form changes

The current parser writes `msgstr` or `msgstr[0]`, not the complete plural array. Use singular messages for this workflow until full Gettext plural support is available.

## Next

* [GitHub localisation workflow](/platform/tutorials/github-localisation-workflow)
* [XLIFF](/platform/tutorials/xliff)
* [`check` reference](/cli/commands/check)
