Remember that standup when your product manager casually dropped "Oh, and we need to support six languages by next month"?
For us devs, unplanned software localization is the rite of passage. I’ve been there too, and I can help you translate PO files without losing your mind.
In this guide, I’ll walk you through the process of localizing software with PO files: figuring out what works, what absolutely doesn't, and how to turn a potential nightmare into a surprisingly smooth process.
What are PO files?
Portable Object (PO) files are simple text documents that store original strings and their translations in pairs. These human-readable files are referenced by Java programs, GNU gettext, and other software as a properties file. They are editable in any text editor, human-readable, and supported by virtually every programming language and framework in existence.
Structure of PO files
PO files are the backbone of the localization process, creating a structured way to separate translatable content from your application code. Here’s what a bare-bones PO file looks like:
#: src/components/Header.js:15
msgid "Sign up for our newsletter"
msgstr "Inscrivez-vous à notre newsletter"
#: src/pages/Product.js:102
#. This text appears on the add-to-cart button
msgid "Add to cart"
msgstr "Ajouter au panier"
Each translation unit has:
- msgid: The source text (what your designers initially wrote)
- msgstr: The translated text (what users will actually see)
- Comments: The lines with #. show context information that helps translators understand where and how the text is used
- Reference paths: Those #: lines showing exactly where each string lives in your codebase
The popularity of PO files can be attributed to their simplicity. You could explain them to your non-technical uncle at Thanksgiving dinner, and he'd probably get it.
PO files been around since before StackOverflow existed, created for the GNU gettext system back when people were still using dial-up internet. Dare I call them the cockroaches of localization formats. They’re simple, resilient, and they'll likely outlive any trendy new i18n system someone's pitching on Hacker News right now.
PO file features
Context specifiers: PO files can include msgctxt fields to disambiguate identical source strings with different meanings:
msgctxt "verb"
msgid "post"
msgstr "publier"
msgctxt "noun"
msgid "post"
msgstr "publication"
Plural forms handling: Different languages have different rules for pluralization. PO files accommodate this with msgid_plural and numbered msgstr variants:
msgid "Found %d error"
msgid_plural "Found %d errors"
msgstr[0] "Trouvé %d erreur"
msgstr[1] "Trouvé %d erreurs"
Translator comments: Special comments starting with #. provide guidance for translators:
#. This is a technical term, please keep it professional
msgid "Kernel panic"
msgstr "Panique du noyau"
Reference information: Comments starting with #: show where strings appear in code, helping translators understand context:
#: src/login.js:42 src/auth.js:77
msgid "Invalid credentials"
msgstr "Identifiants invalides"
PO files vs POT files
Portable Object Template (POT) files serve as templates for creating PO files. POT files contain all original strings without translations. When you need to add a new language, you start with the POT file and fill in the translations, thereby creating PO files.
Alright, with the basics out of the way, let’s localize and translate PO files.
How to translate PO files
Let's break down a workflow that won't make you hate your life. Each step addresses common pitfalls that have tripped up even experienced teams.
Here’s how to translate PO files like a seasoned professional:
Step 0: Create a sample PO file
In case you don’t have PO files yet and only looking into their translation for your software localization project, let’s create a sample PO file to test your workflow. This sanity-check step has saved countless projects from veering off track.
Create a new text file named example.po and add this minimal header information:
msgid ""
msgstr ""
"Project-Id-Version: YourApp 1.0\n"
"POT-Creation-Date: 2025-03-14 15:30+0000\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
Then, add a few representative strings:
#: src/components/Auth/LoginForm.js:42
msgid "Sign in to your account"
msgstr "Connectez-vous à votre compte"
#: src/components/Auth/LoginForm.js:57
msgid "Username"
msgstr "Nom d'utilisateur"
#: src/components/Auth/LoginForm.js:73
msgid "Password"
msgstr "Mot de passe"
#: src/components/Auth/LoginForm.js:92
msgid "Forgot password?"
msgstr "Mot de passe oublié ?"
This sample PO file does three vital things:
- It creates a concrete example you can share with stakeholders and translators.
- It lets you test your loading mechanism before investing hours in full translation.
- It sets the pattern for comments, references, and organization that you'll follow throughout the project.
Step 1: Extract translatable strings (Make automation your friend)
Manual extraction of strings is like copying code with pen and paper—theoretically possible but a horrible idea in practice.
The right extraction tool depends on your stack:
For localizing React or Javascript apps:
# Using react-intl-extract
npm install react-intl-extract --save-dev
npx react-intl-extract --locale=en --outDir=./locales
For Python projects:
xgettext -d myapp -o locales/myapp.pot --from-code=UTF-8 --keyword=_ *.py
For WordPress:
wp i18n make-pot . languages/my-plugin.pot
These tools scan your codebase for marked strings and generate a POT file (PO Template), which contains all your original strings without translations.
The real art lies in how you mark strings in your code. Look at these approaches:
The lazy way (please don't):
// Hardcoded strings everywhere
return <h1>Welcome to our app!</h1>;
The slightly better way:
// Marked but without context
return <h1>{t('Welcome to our app!')}</h1>;
The correct way:
// With context and plural support
return <h1>{t('welcome_header', 'Welcome to our app!')}</h1>;
To put translation disasters in perspective, think of a travel booking site where the word "book" appears throughout the code. In English, it works as both "book a hotel" and "read a book."
But the extraction tool dutifully created a single entry for all instances, because afterall, it’s the same word, right?
The French translators had no context and chose the reading-related translation. Suddenly users were being invited to:
- "livre une chambre" (where livre means “a book”)
- instead of “réserver une chambre” (which means to reserve)
So to avoid these mishaps, protect yourself with these extraction practices:
- Mark strings with unique IDs where possible
- Add comments for ambiguous terms
- Extract regularly during development (not just at the end)
- Use a pre-commit hook to catch unmarked strings
This initial step is the foundation everything else builds on—get it right, and you'll save countless hours of confusion later.
Step 2: Generate language-specific PO files (One file per language)
I also like to create individual files for each language so it’s easier to maintain and separate when multiple translators are working on the team.
With your POT template in hand, you can se command-line tools to create individual PO files:
# For Spanish
msginit --input=messages.pot --locale=es --output=es.po
# For French
msginit --input=messages.pot --locale=fr --output=fr.po
# For Japanese
msginit --input=messages.pot --locale=ja --output=ja.po
Each resulting file contains:
- Your original strings
- Empty slots for translations
- Language-specific metadata (including plural forms rules)
Organization matters more than you might think. A sensible directory structure looks like:
/locales
messages.pot # The template
/en
messages.po
/es
messages.po
/fr
messages.po
Keep your original language in a PO file too. This file will be your fallback and make it easier to update source text later.
Each generated PO file will include the appropriate plural forms rule for the language. For example, the French file will contain:
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
While Japanese, which doesn't use grammatical number in the same way, will have:
"Plural-Forms: nplurals=1; plural=0;\n"
These rules tell the gettext system how to handle plurals in each language, ensuring correct forms are used regardless of the grammatical rules.
If you're updating an existing project, you’d need to merge new strings with existing translations instead of creating new files.
We have the msgmerge tool for this:
msgmerge --update locales/fr.po messages.pot
This preserves existing translations while adding new untranslated strings.
Pro Tip: Never mix multiple language files irrespective of how small your app is today. If your app grows later, you’ll have a hard time changing the code to now extract translation strings from multiple documents instead of just the one.
With your PO files ready, it's time for translation. Let's explore the different approaches in more detail.
Step 3.1: Translate PO file strings manually
Now for the part everyone thinks about first: actual translation. Here’s how to translate PO files manually:
- Open your PO file in VS Code, Sublime, or even Notepad
- Replace the empty msgstr entries with your translations
- Repeat for all entries
- Save the file
For sizable software, manual PO files translation can feel like a full-time job. First, you’ll need to find translators who’ll agree to wade through code to add translations. Then, you’ll have to manage them. Finally, you’ll have to manually take care of the following tasks:
- Provide translation context. Screenshots showing where strings appear reduce errors by up to 70%.
- Create a terminology glossary. List brand names, product terms, and technical concepts that should remain consistent (or untranslated).
- Handle plurals. Some languages can have up to six plural forms that need to be properly translated.
- Account for text expansion. German and Finnish can expand text length by 30-40%. A beautifully designed button with "Send" might not fit "Senden Sie" or "Lähettää".
Too much hassle? There’s an easier way and I’m about to show it.
Step 3.2: Translate PO file strings automatically
Most developer teams don’t handle PO translation manually. Instead, they use localization management platforms, like Centus.
With Centus, you can extract new strings and keep translations synchronized with your codebase. No more manual extraction steps!
Here’s how to translate PO files online:
- Sign up to Centus and click New project
- In the Imports section, click Select a file
- Upload your PO files
Note: Centus also handles other localization file formats like XML, YAML, JSON, PHP, and many more.
Now everything is ready for PO file automatic translation.
In the Centus Editor, you can translate PO files using DeepL, Microsoft Translator, or Google Translate. It goes like this:
- Tick checkboxes next to the keys you want to translate. You can also tick the checkbox at the top to select all keys.
- In the modal that opens, click More options
- Click Machine translate
- Select a machine translation provider and locales
- Click Translate
That’s it! What takes mere clicks, can save you days of work and up to 90% of your translation budget.
Although automatic translations are incredibly accurate these days, you need someone to review them. Here’s how to add professional linguists to your PO translation project:
- In the Contributors section, click Invite contributors
- Enter the team member’s details
- Choose the team member’s role from the dropdown menu
- Click Add contributor
Now your linguists can edit the translations in the convenient Centus Editor without worrying about disrupting msgids or file hierarchy. In the process, they will see automatic glossary suggestions helping them to keep your translations consistent.
Your linguists can also communicate directly in Centus to ensure high translation accuracy.
To keep your project on track, you can monitor the team’s progress in the dashboard.
When PO translations are ready, push them to your code repository using Centus-GitHub integration. It can’t be simpler!
Don’t postpone your PO translation tasks. Try Centus now—no credit card required!
Step 4: Compile your PO files (From human-readable to machine-optimized)
PO files are made for humans. Your application often needs a more optimized format.
For gettext-based systems, compile to binary MO files:
msgfmt -o messages.mo messages.po
For JavaScript applications, JSON is often more practical:
po2json es.po > es.json
For React applications using i18next:
i18next-conv -l es -s es.po -t es.json
Now, can you use PO files directly? I wouldn’t due to other formats being better suited for codified extraction. But well, who’s stopping us from trying?
There are a couple benefits to compiling PO files for localization:
- Improved runtime performance
- Smaller file sizes
- Framework compatibility
- Validation of format correctness
Once you’ve run the compilation, verify that it succeeds without errors. If it fails, you've likely caught a translation problem before it reaches users.
Parting thoughts
The process I’ve shared in this guide grows from years of watching teams struggle with overcomplicated PO translation approaches and then finding simpler paths forward.
Each step—from creating sample files to testing final implementations—addresses common pitfalls before they derail your timeline or budget.
For small apps built for personal use, I’d stick to handling translation manually.
But if you have a large app or plan to grow your app, you need a translation management system like Centus.
Get the week's best content!
By subscribing, you are agreeing to have your personal information managed in accordance with the terms of Centus Privacy Policy ->
Keep learning
22 min. read
Angular with ngx-translate: Practical Guide
18 min. read
7 Localization File Formats Explained with Examples
30 min. read