Translations¶
The plugin ships its own translations, so a site gets them without waiting for a language pack from translate.wordpress.org. The files are in languages/.
Locales¶
de_DE, fr_FR, es_ES, it_IT, nl_NL, pt_BR, ja, ru_RU, pl_PL, zh_CN and he_IL (Hebrew, which also exercises right-to-left layouts).
The list, with the plural forms of each locale (the values translate.wordpress.org uses), is in one place: the locales variable at the top of bin/translations.sh. To add a locale, add a line there, run make translations-edit, translate the file, then run make translations.
The files¶
The repository holds one file per locale and nothing else: languages/wp-updates-notifier-<locale>.l10n.php, the format WordPress 6.5 and later loads by default (the plugin needs 6.7). It is the source of truth. There is no committed template (.pot), .po or .mo file: translate.wordpress.org scans the code for strings itself, and the gettext files are made in the git-ignored build/translations/ when a translating session needs them. bin/l10n-to-po.php turns a .l10n.php file back into a PO file.
Never edit a .l10n.php file by hand: make check-translations fails unless it is byte for byte what the translations and the code build. The build is deterministic: the header holds no date (WP-CLI would otherwise write the current time into the file), so running it twice changes nothing.
Status: machine-translated, not yet reviewed. All the translations were made by an AI assistant in Claude Code sessions and have not been reviewed by a native speaker. The .l10n.php format cannot carry a marker for that, so this page and ADR 0017 record it. They stay so until the translations are imported to translate.wordpress.org and reviewed there, which needs a project translation editor (#58).
Commands¶
All run in Docker. Add LOCALE=de_DE to work on one locale.
| Command | What it does |
|---|---|
make translations-edit |
Generate the template from the code, export each .l10n.php file to a PO file in build/translations/<locale>.po (overwriting it) and merge it with the template. New strings come in empty, strings that left the code are dropped, a changed string is not guessed from a similar one, and the translators comments (#.) and contexts are there to read. It prints the number of untranslated and fuzzy entries per locale. A translating session fills the empty msgstr entries. |
make translations-todo |
Print the untranslated and fuzzy entries of each locale in PO format, with a count per locale. |
make translations |
Build languages/*.l10n.php from the PO files, merged with the template again, with the fixed header and the plural forms from the table in bin/translations.sh. |
make check-translations |
Fail unless, for every locale, msgfmt -c --check-format passes, nothing is untranslated, fuzzy or obsolete (a translation of a string no longer in the code), the plural forms and language are right, and the file is byte for byte what a fresh build gives. It also fails on make-pot warnings and on any other file in languages/. CI runs it in the phpcs job of lint.yml. |
Which PO file translations-todo and translations read: build/translations/<locale>.po when it is newer than the locale's .l10n.php (a session is editing it), otherwise a fresh export of the .l10n.php. So a leftover edited file never overwrites newer committed translations. translations-edit always overwrites it, and translations leaves it in place, so a session can build again after a fix.
Changing a string¶
Any change to a translatable string, or to its translators comment, needs a translation session before CI is green:
make translations-edit- Translate the empty entries in
build/translations/*.po(keep placeholders such as%sand%1$sexactly, and fill every plural form of a plural entry). make translations, thenmake check-translations.- Commit the
.l10n.phpfiles.
How the files are loaded¶
WordPress looks for a plugin's translations in wp-content/languages/plugins (the language packs it installs) and resolves one directory per text domain and locale, with the language packs first. Packs from translate.wordpress.org exist for some locales (German, Spanish, Japanese and Polish, among others) and were built from the 1.x strings. An installed pack would therefore hide the complete bundled file.
So WPUN\Translations does two things:
- The
lang_dir_for_domainfilter (WordPress 6.6 and later) points WordPress at the bundledlanguages/directory for every locale the plugin ships. - On
load_textdomain, just before WordPress loads the bundled file, it loads the language pack of the same locale first.
WordPress looks a string up in the files of a text domain in the order they were loaded and takes the first hit. A string in the language pack wins, and the bundled file fills every gap. A locale the plugin does not ship is left to WordPress. The bundled directory holds only .l10n.php files, so a site that forces the MO format with the translation_file_format filter gets no bundled strings; a language pack counts in either format. Loading stays just in time: nothing is loaded until the first string is translated, which is after init. The plugin does not call load_plugin_textdomain(), as WordPress loads the files itself.
The tests are in tests/wp/test-translations.php. They build a fake bundled file and a fake language pack (as .l10n.php and as .mo) for a locale that does not exist.