Product
Services
Solutions
Blog
Back

PO/POT/MO (Gettext) Format Guide

Contents

GNU Gettext is one of the most widely adopted internationalization systems, using PO (Portable Object), POT (Portable Object Template), and MO (Machine Object) files for managing translations across multiple platforms and programming languages.

What is Gettext format

Gettext is GNU's internationalization and localization system that provides a standardized way to handle multilingual applications. It consists of three main file types that work together to manage translations.

File types overview

File Type

Extension

Purpose

Usage

POT

.pot

Template file

Contains source strings for extraction

PO

.po

Translation file

Human-readable translations

MO

.mo

Binary file

Compiled translations for runtime

Key characteristics

  • Cross-platform compatibility — Works with PHP, Python, JavaScript, C++, and more

  • Human-readable format — PO files can be edited with any text editor

  • Professional workflow — Widely used by professional translation teams

  • Plural support — Built-in handling of complex plural rules

  • Context support — Disambiguation for identical strings

File structure and syntax

Basic PO file structure

# Translation file header
msgid ""
msgstr ""
"Project-Id-Version: MyApp 1.0\n"
"POT-Creation-Date: 2024-01-15 10:30+0000\n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

# Translator comment
#. Developer comment
#: source_file.php:25
msgid "Hello, world!"
msgstr "Hello, world!"

# Another translation
#: main.js:42
msgid "Save changes"
msgstr "Save changes"

Entry components

  1. Comments — Different types for various purposes

  2. msgid — Source string identifier

  3. msgstr — Translated string

  4. Context — Optional disambiguation

  5. References — Source file locations

Comment types

# Translator comment - for translators
#. Extracted comment - from source code
#: file.php:123 - source reference
#, fuzzy - flags (fuzzy, c-format, etc.)
#| msgid "old text" - previous string for reference

Meta-information and headers

Standard headers

msgid ""
msgstr ""
"Project-Id-Version: MyProject 2.1\n"
"Report-Msgid-Bugs-To: support@example.com\n"
"POT-Creation-Date: 2024-01-15 10:30+0000\n"
"PO-Revision-Date: 2024-01-16 14:45+0100\n"
"Last-Translator: John Doe <john@example.com>\n"
"Language-Team: English <en@example.com>\n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

Header explanations

Header

Purpose

Example

Project-Id-Version

Project name and version

MyApp 1.0

POT-Creation-Date

When template was created

2024-01-15 10:30+0000

PO-Revision-Date

Last translation update

2024-01-16 14:45+0100

Last-Translator

Translator information

John Doe <john@example.com>

Language

Target language code

en, es, fr-CA

Plural-Forms

Plural rule definition

nplurals=2; plural=(n != 1);

Plural forms in Gettext

Basic plural structure

msgid "You have %d message"
msgid_plural "You have %d messages"
msgstr[0] "You have %d message"
msgstr[1] "You have %d messages"

Complex plural forms (Russian example)

# Russian: 4 plural forms
msgid "%d file"
msgid_plural "%d files"
msgstr[0] "%d файл"
msgstr[1] "%d файла"
msgstr[2] "%d файлов"
msgstr[3] "%d файла"

Plural rules for different languages

Language

nplurals

Plural expression

Forms

English

2

(n != 1)

1 item, 2 items

Russian

4

(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3)

1 файл, 2 файла, 5 файлов, 1.5 файла

Polish

4

(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3)

1 plik, 2 pliki, 5 plików, 1.5 pliku

Arabic

6

Complex formula

0, 1, 2, 3-10, 11-99, 100+

Plural forms examples

# English (2 forms)
msgid "Delete %d item?"
msgid_plural "Delete %d items?"
msgstr[0] "Delete %d item?"
msgstr[1] "Delete %d items?"

# Russian (4 forms)
msgid "%d new notification"
msgid_plural "%d new notifications"
msgstr[0] "%d новое уведомление"
msgstr[1] "%d новых уведомления"
msgstr[2] "%d новых уведомлений"
msgstr[3] "%d новых уведомления"

# Polish (4 forms)
msgid "Downloaded %d file"
msgid_plural "Downloaded %d files"
msgstr[0] "Pobrano %d plik"
msgstr[1] "Pobrano %d pliki"
msgstr[2] "Pobrano %d plików"
msgstr[3] "Pobrano %d pliku"

Working with MO files

What are MO files

MO (Machine Object) files are binary compiled versions of PO files, optimized for fast runtime lookup by applications.

Characteristics:

  • Binary format — Not human-readable

  • Fast lookup — Optimized for application performance

  • Compact size — Smaller than PO files

  • Runtime usage — Used by applications, not translators

Compilation process

# Compile PO to MO
msgfmt messages.po -o messages.mo

# Validate PO file before compilation
msgfmt --check messages.po

# Compile with statistics
msgfmt --statistics messages.po -o messages.mo

File organization

locale/
├── en/
│   └── LC_MESSAGES/
│       ├── messages.po
│       └── messages.mo
├── es/
│   └── LC_MESSAGES/
│       ├── messages.po
│       └── messages.mo
└── fr/
    └── LC_MESSAGES/
        ├── messages.po
        └── messages.mo

Integration with Localit.io

Uploading Gettext files

Supported formats:

  • PO files — Primary translation format

  • POT files — Template files with source strings

  • MO files — Binary files (readable but not editable)

Upload features:

  • Automatic header parsing — Extracts metadata and plural rules

  • Comment handling — Preserves translator and extracted comments

  • Encoding detection — Supports UTF-8, Latin-1, and other encodings

Advanced import options

✅ Import settings:
- Preserve translator comments
- Import source references (#:)
- Handle fuzzy translations
- Extract plural forms automatically

Export capabilities

Standard export:

  • Complete PO structure — Headers, comments, references

  • Plural forms — Automatic plural rule generation

  • Encoding options — UTF-8, custom encodings

Advanced export options:

  • POT template generation — Create templates from translations

  • MO compilation — Generate binary files

  • Custom headers — Project-specific metadata

  • Reference filtering — Include/exclude source references

Localit.io specific features

Custom plural key support

In Localit.io, the msgid_plural field can be customized:

# Standard approach
msgid "file_count"
msgid_plural "file_count"
msgstr[0] "%d file"
msgstr[1] "%d files"

# Custom plural key
msgid "file_count"
msgid_plural "files"
msgstr[0] "%d file"
msgstr[1] "%d files"

File format examples

Complete PO file example

# Translation file for MyApp
# Copyright (C) 2024 Company Name
# This file is distributed under the same license as the MyApp package.
#
msgid ""
msgstr ""
"Project-Id-Version: MyApp 1.2\n"
"Report-Msgid-Bugs-To: bugs@example.com\n"
"POT-Creation-Date: 2024-01-15 10:30+0000\n"
"PO-Revision-Date: 2024-01-16 14:45+0100\n"
"Last-Translator: Maria Garcia <maria@example.com>\n"
"Language-Team: Spanish <es@example.com>\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

# Main navigation
#. Menu item for home page
#: templates/header.php:15
msgid "Home"
msgstr "Inicio"

#. Menu item for user profile
#: templates/header.php:16
msgctxt "navigation"
msgid "Profile"
msgstr "Perfil"

# User management
#. Profile page title
#: pages/profile.php:25
msgctxt "page_title"
msgid "Profile"
msgstr "Perfil de Usuario"

# Notifications with plurals
#: js/notifications.js:42
msgid "You have %d new message"
msgid_plural "You have %d new messages"
msgstr[0] "Tienes %d mensaje nuevo"
msgstr[1] "Tienes %d mensajes nuevos"

# File operations
#: utils/filemanager.php:156
msgid "Delete %d selected file?"
msgid_plural "Delete %d selected files?"
msgstr[0] "¿Eliminar %d archivo seleccionado?"
msgstr[1] "¿Eliminar %d archivos seleccionados?"

# Error messages
#. Network connection error
#: api/client.js:89
msgid "Connection failed. Please try again."
msgstr "Falló la conexión. Por favor, inténtalo de nuevo."

# Status messages
#, fuzzy
#: components/upload.js:234
msgid "Upload completed successfully"
msgstr "Subida completada exitosamente"

POT template example

# Template file for MyApp
# Copyright (C) 2024 Company Name
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-01-15 10:30+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

#: templates/header.php:15
msgid "Home"
msgstr ""

#: templates/header.php:16
msgctxt "navigation"
msgid "Profile"
msgstr ""

#: js/notifications.js:42
msgid "You have %d new message"
msgid_plural "You have %d new messages"
msgstr[0] ""
msgstr[1] ""

Best practices for Gettext

String extraction

Use meaningful msgids:

✅ Good:
msgid "save_button"
msgid "error_network_timeout"
msgid "user_profile_updated"

❌ Avoid:
msgid "btn1"
msgid "err"
msgid "msg"

Provide context when needed:

✅ Good:
msgid "save_button"
msgstr "Save"

msgid "save_menu_item"  
msgstr "Save As..."

❌ Ambiguous:
msgid "Save"  # Which Save context?

Translation workflow

Comment guidelines:

# Translator comment: Explain cultural context
#. Developer comment: Technical details
#: source.php:123  # Automatic reference
#, fuzzy  # Needs review

Plural handling:

# Always test edge cases
msgid "%d item found"
msgid_plural "%d items found"
# Test: 0, 1, 2, 11, 21, 101, etc.
msgstr[0] "%d elemento encontrado"
msgstr[1] "%d elementos encontrados"

File organization

Directory structure:

locales/
├── messages.pot           # Template
├── en/
│   └── LC_MESSAGES/
│       ├── messages.po    # English translations
│       └── messages.mo    # Compiled binary
├── es/
│   └── LC_MESSAGES/
│       ├── messages.po    # Spanish translations
│       └── messages.mo    # Compiled binary
└── fr/
    └── LC_MESSAGES/
        ├── messages.po    # French translations
        └── messages.mo    # Compiled binary

Domain separation:

├── messages.po           # Main application
├── admin.po             # Admin interface
├── errors.po            # Error messages
└── emails.po            # Email templates

Common issues and troubleshooting

Encoding problems

Wrong encoding declaration:

❌ Problem:
"Content-Type: text/plain; charset=ISO-8859-1\n"
# But file contains UTF-8 characters

✅ Solution:
"Content-Type: text/plain; charset=UTF-8\n"
# Ensure file is actually saved as UTF-8

Character corruption:

  • Always use UTF-8 encoding for modern applications

  • Validate encoding before importing

  • Check BOM (Byte Order Mark) handling

Plural form errors

Wrong plural count:

❌ Russian with 2 forms:
msgstr[0] "файл"
msgstr[1] "файлов"  # Missing middle forms

✅ Russian with 4 forms:
msgstr[0] "файл"
msgstr[1] "файла"
msgstr[2] "файлов"
msgstr[3] "файла"

Incorrect plural expression:

❌ Wrong:
"Plural-Forms: nplurals=2; plural=(n > 1);\n"

✅ Correct for English:
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

✅ Correct for Russian:
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"

Syntax errors

Missing quotes:

❌ Syntax error:
msgid Save changes
msgstr Guardar cambios

✅ Correct:
msgid "Save changes"
msgstr "Guardar cambios"

Unescaped quotes:

❌ Incorrect:
msgid "He said "Hello!""

✅ Correct:
msgid "He said \"Hello!\""

Platform-specific implementations

PHP frameworks

WordPress:

// wp-content/themes/mytheme/languages/mytheme.po
msgid "Read more"
msgstr "Leer más"

// Usage in template
echo __('Read more', 'mytheme');
echo _n('%d comment', '%d comments', $count, 'mytheme');

Laravel:

// resources/lang/es/messages.po
msgid "validation.required"
msgstr "El campo :attribute es obligatorio."

// Usage in controller
echo __('validation.required');
echo trans_choice('messages.items', $count);

Symfony:

// translations/messages.es.po
msgid "form.submit"
msgstr "Enviar"

// Usage in Twig template
{{ 'form.submit'|trans }}
{{ 'items.count'|transchoice(count) }}

Python frameworks

Django:

# locale/es/LC_MESSAGES/django.po
msgid "Welcome to our site"
msgstr "Bienvenido a nuestro sitio"

# Usage in views/templates
from django.utils.translation import gettext as _
message = _("Welcome to our site")

# In templates
{% load i18n %}
{% trans "Welcome to our site" %}
{% blocktrans count counter=items|length %}
    {{ counter }} item
{% plural %}
    {{ counter }} items
{% endblocktrans %}

Flask:

# babel.cfg
[python: **.py]
[jinja2: **/templates/**.html]

# translations/es/LC_MESSAGES/messages.po
msgid "Hello, %(username)s!"
msgstr "¡Hola, %(username)s!"

# Usage in Flask app
from flask_babel import gettext, ngettext
message = gettext('Hello, %(username)s!', username=user.name)
count_msg = ngettext('%(num)d item', '%(num)d items', count)

JavaScript frameworks

React with i18next:

// public/locales/es/translation.json
// Converted from PO files
{
  "welcome": "Bienvenido",
  "items": "{{count}} elemento",
  "items_plural": "{{count}} elementos"
}

// Component usage
import { useTranslation } from 'react-i18next';
const { t } = useTranslation();
return <h1>{t('welcome')}</h1>;

Vue.js with vue-i18n:

// locales/es.js (from PO conversion)
export default {
  message: {
    hello: 'Hola mundo',
    items: '{count} elemento | {count} elementos'
  }
}

// Component usage
<template>
  <p>{{ $t('message.hello') }}</p>
  <p>{{ $tc('message.items', count) }}</p>
</template>

Angular:

// src/locale/messages.es.xlf (can be generated from PO)
<trans-unit id="welcome" datatype="html">
  <source>Welcome</source>
  <target>Bienvenido</target>
</trans-unit>

// Component usage
import { Component } from '@angular/core';
@Component({
  template: `<p i18n="@@welcome">Welcome</p>`
})

Node.js implementations

Express with node-gettext:

// locales/es/LC_MESSAGES/messages.po
msgid "user.created"
msgstr "Usuario creado exitosamente"

// Usage in Express
const Gettext = require('node-gettext');
const gt = new Gettext();
gt.addTranslations('es', 'messages', require('./locales/es/messages.json'));
app.get('/api/user', (req, res) => {
  gt.setLocale('es');
  res.json({ message: gt.gettext('user.created') });
});

Koa.js:

// middleware/i18n.js
const Koa = require('koa');
const i18n = require('koa-i18n');
const app = new Koa();

app.use(i18n(app, {
  directory: './locales',
  extension: '.po',
  locales: ['en', 'es', 'fr']
}));

// Usage in routes
ctx.body = ctx.i18n.__('welcome.message');

CMS integrations

Drupal:

// sites/all/modules/custom/mymodule/translations/es.po
msgid "Content created successfully"
msgstr "Contenido creado exitosamente"

// Usage in module
drupal_set_message(t('Content created successfully'));
format_plural($count, '1 item', '@count items');

Joomla:

// language/es-ES/es-ES.com_mycomponent.po
msgid "COM_MYCOMPONENT_SAVE"
msgstr "Guardar"

// Usage in component
echo JText::_('COM_MYCOMPONENT_SAVE');
echo JText::plural('COM_MYCOMPONENT_ITEMS', $count);

TYPO3:

// Resources/Private/Language/es.locallang.po
msgid "button.save"
msgstr "Guardar"

// Usage in templates
<f:translate key="button.save" />
{f:translate(key: 'items.count', arguments: {0: count})}

Mobile frameworks

React Native:

// locales/es/common.po converted to JSON
{
  "welcome": "Bienvenido",
  "notifications": "{{count}} notificación",
  "notifications_plural": "{{count}} notificaciones"
}

// Usage in components
import i18n from 'i18next';
<Text>{i18n.t('welcome')}</Text>
<Text>{i18n.t('notifications', { count: notificationCount })}</Text>

Flutter (Dart):

// lib/l10n/app_es.arb (converted from PO)
{
  "welcome": "Bienvenido",
  "itemCount": "{count, plural, =0{Sin elementos} =1{1 elemento} other{{count} elementos}}"
}

// Usage in widgets
Text(AppLocalizations.of(context)!.welcome)
Text(AppLocalizations.of(context)!.itemCount(count))

Build tools and automation

Webpack integration:

// webpack.config.js
const path = require('path');
module.exports = {
  module: {
    rules: [
      {
        test: /\.po$/,
        use: [{
          loader: 'po-loader',
          options: {
            format: 'jed'
          }
        }]
      }
    ]
  }
};

Gulp workflow:

// gulpfile.js
const gulp = require('gulp');
const po2json = require('gulp-po2json');

gulp.task('i18n', () => {
  return gulp.src('locales/**/*.po')
    .pipe(po2json())
    .pipe(gulp.dest('dist/locales'));
});

Vite plugin:

// vite.config.js
import { defineConfig } from 'vite';
import { gettext } from 'vite-plugin-gettext';

export default defineConfig({
  plugins: [
    gettext({
      input: 'locales/**/*.po',
      output: 'dist/locales'
    })
  ]
});

Migration and compatibility

From other formats

From JSON:

// JSON
{
  "save_button": "Save",
  "items_count": {
    "one": "%d item",
    "other": "%d items"
  }
}

// PO equivalent
msgid "save_button"
msgstr "Save"

msgid "%d item"
msgid_plural "%d items"
msgstr[0] "%d item"
msgstr[1] "%d items"

From CSV:

key,english,spanish
save_button,Save,Guardar
cancel_button,Cancel,Cancelar

# PO equivalent
msgid "save_button"
msgstr "Guardar"

msgid "cancel_button"
msgstr "Cancelar"

Legacy considerations

Version compatibility:

  • GNU Gettext 0.18+ — Full feature support

  • Older versions — Limited plural support

  • Platform variants — Some features may differ

Migration best practices:

  1. Backup existing files before conversion

  2. Validate syntax after importing

  3. Test plural forms thoroughly

  4. Verify encoding compatibility

Conclusion

The Gettext format provides a robust, industry-standard solution for internationalization with excellent tooling support and wide platform compatibility. Localit.io's comprehensive Gettext support, including advanced plural handling, context management, and custom plural keys, makes it an ideal choice for professional translation workflows.

Key advantages:

  • Industry standard — Widely adopted and supported

  • Professional tooling — Extensive ecosystem of tools

  • Plural support — Comprehensive plural form handling

  • Context disambiguation — Built-in context support

  • Binary optimization — MO files for runtime performance

  • Cross-platform — Works with virtually any programming language

Grow your business with the help of Localit

Make localization a breeze with our service! Streamline translations, boost collaboration, and maintain a consistent brand style across all your content!
Try for free
Ready to simplify your localization process?
Transform your localization process with Localit! Simplify translations, automate workflows, and boost your global reach effortlessly.
Start for Free Today
Alex Abakumov
Сopywriter Localit
Alex Abakumov is a skilled B2B and B2C copywriter based in Brussels with 4+ years of experience crafting compelling content. When not writing, he’s walking his German shepherd, traveling to new places, or trying to get the hottest music tickets in town.