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 |
| Template file | Contains source strings for extraction |
PO |
| Translation file | Human-readable translations |
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
-
Comments — Different types for various purposes
-
msgid — Source string identifier
-
msgstr — Translated string
-
Context — Optional disambiguation
-
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 name and version |
|
| When template was created |
|
| Last translation update |
|
| Translator information |
|
| Target language code |
|
| Plural rule definition |
|
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 |
| 1 item, 2 items |
Russian | 4 |
| 1 файл, 2 файла, 5 файлов, 1.5 файла |
Polish | 4 |
| 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:
-
Backup existing files before conversion
-
Validate syntax after importing
-
Test plural forms thoroughly
-
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