String Translation
String Translation lets you override gettext strings emitted by themes and plugins from inside WordPress - no editing .po/.mo files, no rebuilding, no lost changes after updates. PerfLocale scans PHP source for __(), _e(), _x(), _n(), and every other WordPress i18n function, stores what it finds, and serves your translations through the standard gettext filter.
What it handles
__(),_e(),esc_html__(),esc_attr__(),esc_html_e(),esc_attr_e()- simple strings_x(),_ex(),esc_html_x(),esc_attr_x()- context-disambiguated strings_n(),_nx(),_n_noop(),_nx_noop()- plural forms, including languages with three to six CLDR plural forms (e.g. Russian, Polish, Arabic), not just a two-formn == 1/ else split
Scanning
The scanner runs from PerfLocale → Strings → Scan for Strings (top-right button). It walks the parent theme, the child theme (if any), every active plugin directory and mu-plugins/ - PerfLocale’s own directory is deliberately skipped so its strings stay out of your catalogue - reads every PHP file, and inserts newly-found strings into the perflocale_strings table. Existing strings are preserved; only new ones are added. Files larger than 2 MB are skipped to avoid memory spikes, and vendor/, node_modules/, .git/, tests/, test/, cache/, build/ and dist/ are excluded by default (filter perflocale/strings/scanner/excluded_paths).
A full scan on a site with many active plugins can take a minute or two. PerfLocale dispatches the scan via its background-jobs system - you'll be redirected to PerfLocale → Jobs where progress and any errors are visible live. The scan always runs as a background job, whatever the Background Processing setting. When it would need longer than one web request on your host, it is refused with the WP-CLI command to run instead (see Background Jobs).
You can also run the scanner from the command line. The CLI scans one directory at a time and defaults to the active theme, so pass --dir to point it elsewhere:
# Active theme (default).
wp perflocale strings scan
# A specific plugin, restricted to one text domain.
wp perflocale strings scan --dir=wp-content/plugins/my-plugin --domain=my-pluginOn SQLite (WordPress Playground, and hosts that use the SQLite Database Integration plugin), searching and sorting on the Strings screen match upper and lower case only for unaccented Latin letters and do not ignore accents: a search for e does not find é, and É and é count as different. This is a limit of the SQLite Database Integration, which WordPress’s own search shares; on MySQL and MariaDB both are ignored.
Domains and contexts
Every string has a text domain (usually a theme or plugin slug) and an optional context. Filter the Strings page by domain (e.g. only translate woocommerce strings) and combine with context when the same word has different meanings (e.g. Post as a noun vs. a verb).
Translating a string
Click any row on the Strings page. The editor opens with the original on the left and one editable field per active language on the right. Plural strings appear as two rows - singular text and plural text - and for languages with more than two plural forms the plural row adds an extra field per grammatical form. Save - the translation takes effect immediately on frontend pages. Machine translation is available via the MT button if you’ve configured a provider.
A save writes only the cells you changed. A cell that changed after you opened the page is left as it is when you did not edit it. When you did edit it, the outcome depends on who changed it. Your edit wins over machine translation: a machine translation that a run wrote into the cell since you opened the page is replaced by your edit, and a cleared cell is cleared. Another person’s change is reported: when someone else saved the cell, or a PO file was imported into it, your edit is not written and the page reports the cell, for example “1 translation was not saved because someone else changed it after you opened this page: "Add to cart" (Deutsch). Your edit was discarded: check the current text, then edit it again.” A translation you save this way carries the manual label, also when a machine translation run is still working on the same string.
Two other refusals tell you to act. “Translations not saved: the form lacked the data needed to check for changes others made since you opened it (for example a page opened before an update). Reload the page, then edit and save again.” means the page was opened before an update of PerfLocale; reload it. “1 translation was not saved: the database stores the text in another character set than it compares it in, so the save could not check whether someone else changed it. Ask your host to convert the PerfLocale tables to utf8mb4, then save again.” names the cause and the fix.
Plural forms
WordPress plural calls - _n(), _nx(), and the _n_noop() / _nx_noop() variants - are served with each language’s full CLDR plural rules, not just a two-form n == 1 / else split. Each language picks its own form set at runtime through WordPress core’s \Plural_Forms: Japanese has 1 form, English and German 2, Russian and Polish 3, Arabic 6.
For languages with more than two forms, the plural row on the Strings screen shows one extra input per grammatical form, so a translator fills in each CLDR category (Polish few vs. many, for example) directly. Multi-form .po files import correctly - every msgstr[…] form is stored.
PO round-trip: a .po export writes each plural string as one gettext plural entry (msgid and msgid_plural) with as many msgstr[…] forms as the language has, including the forms beyond the standard two, so exporting a file and importing it again keeps every form. An untranslated plural string is exported with empty forms for a translator to fill in. When the export cannot pair a plural text with its singular, its extra forms are not in the file: wp perflocale po-export warns and names the first string ids, a Merge import of that file keeps those forms and a Replace import removes them. The JSON data export is the lossless backup of every string translation.
Storage modes: database vs files
PerfLocale supports two storage backends for string translations, selectable at Settings → Performance:
- Files (default) - generates
.l10n.phptranslation files (the format WP 6.5+ prefers) inwp-content/uploads/perflocale/translations/. Same speed as a native language pack; zero DB reads for gettext lookups; regenerated automatically when you edit a string. - Database - translations live in
perflocale_string_translationswith a 3-layer cache in front. Simple; every edit is instantly live; portable across hosts via the Export feature.
Files mode compiles files for the active languages other than the default, which serves the original strings. The files are rebuilt once, at the end of the request, when a language is activated, when an active language’s locale changes and when the default language changes; after a plugin update, one background run writes any file the site is missing. Tools → Site Health names each language whose translations have no compiled file (“Compiled translation files are missing for: …”).
Language fallback chains apply to strings in database mode only.
Regenerate Translation Files
If you ever see Tools → Site Health reporting “Some translations need to be re-linked” or “Compiled translation files are missing for: …”, the fix is a single click: Settings → Performance → Regenerate Translation Files. That button does three things in one pass:
- Self-heal. Any saved translation that has lost its connection to the file-generation pipeline (orphaned
string_translationsrow, string with no group, or a missingtranslation_linksrow) is reconnected automatically before generation starts. The completion notice reports how many translations were reconnected on this run. - Clean. Stale
.l10n.phpfiles are removed. - Generate. Fresh files are written, one per (domain × locale) pair.
In normal operation a save on the Strings screen rebuilds the files of the languages it changed, so you rarely need to click the button. The manual path exists for two cases: (a) recovering from a partial import or a re-scan that renumbered strings, and (b) after editing translations directly in the database (e.g. via a migration script that bypassed the normal save flow).
Import / export
Export your string translations as standard .po files straight from PerfLocale → Strings - the Export PO and Import PO buttons sit beside the page heading, one language at a time - or from the command line with wp perflocale po-export / po-import. The full JSON export at Settings → Export & Import carries the same data under its String Translations section. See Export & Import.
A PO import with Replace existing translations for this language ticked (po-import --replace on the command line) is all or nothing: when the database refuses a write during the import, everything it changed is rolled back and the import says so, for example “Replace import rolled back: the database refused a write, so nothing was changed.”
Plural strings are exported as gettext plural entries with every form the language has, and multi-form PO files import every form, so a PO export and re-import keeps all plural forms (see Plural forms). For a lossless backup of every string translation, use the JSON data export.
Imports by accounts without unfiltered_html
A PO file uploaded on PerfLocale → Strings is stored at the trust level of the account that uploads it. For an account without WordPress’s unfiltered_html capability, each translation goes through the same HTML filter the Strings screen applies when you save a translation there (WordPress’s wp_kses_post()), which keeps the markup WordPress allows in post content.
Only values that can contain markup are touched: a translation with no < in it is stored exactly as the file has it. A translation that does contain markup can come back reformatted, as it would after a save on the Strings screen - single-quoted attributes become double-quoted, <br/> becomes <br />, a bare & becomes & - and a <…> the filter does not read as an allowed tag is removed, including a printf placeholder that stands where an attribute would (the %2$s in <a href="%1$s" %2$s>). Entries with no translation (msgid only) and fuzzy entries are skipped as they always are, and are not filtered.
When the filter leaves nothing of a translation, a message says which one:
- A whole entry emptied - Not imported: removing HTML that is not allowed left the translation of "…" empty. The entry is skipped, so in Merge mode whatever was already stored for that string stays as it was.
- One plural form emptied - a Partly imported: … message names the entry and the
msgstr[…]index. An emptiedmsgstr[0]ormsgstr[1]is not written; formsgstr[1]that includes the extra plural forms that come after it. In Merge mode the message for an emptiedmsgstr[0]ormsgstr[1]ends Any translation already stored for it was kept. An emptiedmsgstr[2]or later is different: it is stored as an empty form in its own slot, so the forms after it keep their places, and it replaces whatever that form held before, in Merge mode too. - Every importable translation in the file emptied - Nothing was imported: removing HTML that is not allowed left every importable translation in the PO file empty. The import stops before it writes anything, so in Replace mode the language’s existing translations are not deleted.
The notice after the import counts both cases - … imported entries were changed by the HTML filter for accounts without the unfiltered_html capability … for entries that were imported after filtering, and … entries were not imported because nothing was left after removing HTML that is not allowed - and lists the first three messages under the counts.
Accounts that hold unfiltered_html - on a single site that includes administrators, unless DISALLOW_UNFILTERED_HTML is set - store each translation exactly as the file has it. wp perflocale po-import always does, whichever user it runs as.
The JSON import on Settings → Export & Import applies the same filter to its String Translations section, with two differences. A JSON translation the filter empties is still stored, empty, and the site treats it as untranslated; the JSON import does not stop when that happens to every translation, so a Replace import goes ahead. And the JSON import reports only how many rows were changed, not a message per entry. See Export & Import.
Related
- Machine Translation - auto-translate untranslated strings.
- WP-CLI Commands -
wp perflocale strings scan,po-export,po-import. - Production Tuning - string-translation caching strategy.