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
.pofile and how to read one - Where a module ships its translations, and the naming convention Drupal expects
- How to import a
.pofile, both through the UI and viadrush - The difference between a
.pofile (translations) and a.potfile (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.
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
msgidreads"Hello, @name!"but a translator's.pofile hasmsgid "Hello @name!"(missing the comma), will that translation get used? (Answer: no —msgidmust 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
.pofile'smsgidmust exactly match the original string passed tot()/$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
.pottemplate to hand to translators; import a completed.pofile to actually apply their work. drush locale:importlets 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.