Defining .po files for module translationsfor Drupal 8 , 9 , 10 , and 11

Last updated :  

Wrapping strings in $this->t() tells Drupal "this text can be translated" — it doesn't supply the actual translations. That's the job of a .po file (Portable Object): a plain-text format, shared across the whole gettext/localization ecosystem (not a Drupal invention), that maps each original string to its translated equivalent for one specific language.

What you'll learn in this lesson

  • The real structure of a .po file and how to read one
  • Where a module ships its translations, and the naming convention Drupal expects
  • How to import a .po file, both through the UI and via drush
  • The difference between a .po file (translations) and a .pot file (the template listing what needs translating)

Anatomy of a .po file

msgid "Your settings have been saved."
msgstr "Vos paramètres ont été enregistrés."

msgid "Hello, @name! You have @count new messages."
msgstr "Bonjour, @name ! Vous avez @count nouveaux messages."

msgid is the exact original string as it appears in your $this->t() call — character for character, including the @name/@count placeholders left untouched. msgstr is the translated text, with the same placeholder tokens repositioned wherever the target language's grammar needs them. Drupal substitutes the real values into whichever string — original or translated — actually gets used at render time.

Where translations live in your module

my_module/
└── translations/
    └── fr.po

A file named after its language code (fr.po for French, de.po for German) inside a translations/ directory in your module root is picked up automatically by Drupal's Interface Translation system once that language is added to the site and you run an update, or import it manually at admin/config/regional/translate/import.

.po vs. .pot — translations vs. the template

A .pot file (Portable Object Template) is a .po file with every msgstr left blank — it's the extracted list of every translatable string in your module, generated by a tool like Potx or drush locale:export, and handed to a translator as a starting point. They fill in each msgstr and send back a real .po file per language. You'll generate .pot files when you're the one requesting translations; you'll write or receive .po files when supplying them.

Import via drush, for real workflows: drush locale:import fr /path/to/fr.po --type=customized imports a file programmatically — essential for CI/deployment pipelines where clicking through the admin UI isn't an option. The --type=customized flag marks these as manual overrides so a future community-translation sync won't silently replace them.

Quick check: if your module's msgid reads "Hello, @name!" but a translator's .po file has msgid "Hello @name!" (missing the comma), will that translation get used? (Answer: no — msgid must match the original string byte-for-byte, including punctuation. Even a single missing comma means Drupal treats it as a completely different, untranslated string.)

Key takeaways

  • A .po file's msgid must exactly match the original string passed to t()/$this->t(), placeholders included, or the translation silently won't apply.
  • Ship translations under your_module/translations/<langcode>.po — Drupal's Interface Translation system looks there automatically.
  • Generate a .pot template to hand to translators; import a completed .po file to actually apply their work.
  • drush locale:import lets translation imports run as part of a deployment pipeline instead of a manual admin-UI click.

Coming up next

Interface strings aren't the only thing that needs translating — a site's own configuration (a block's custom text, a config entity's label) often does too. Next, we cover config translation: how to make configuration values themselves translatable, on top of the interface strings you've just learned to ship.