This article traces the full path from a stored translation row to a translated label rendered on the page: how MO files are compiled, where they land on disk, how the plugin injects them into WordPress’s textdomain loader, and how runtime gettext filters behave. It is the developer companion to the user-facing article. For product positioning see multi-language real estate website.
Relevant Files
| File | Role |
|---|---|
| includes/translation-directories.php | Candidate list + writable-directory selection + caching. |
| includes/translation-files.php | Filename builder, locale normaliser, loaded-file registry. |
| includes/translation-runtime.php | Active-locale resolver, DB lookup cache, admin-only runtime provider. |
| includes/translation-hooks.php | Filter callbacks attached to load_textdomain_mofile, override_load_textdomain, gettext, gettext_with_context. |
| includes/admin/string-export.php | Compiles DB rows into MO files via the WordPress POMO classes. |
| includes/plugin-bootstrap.php | Registers the four gettext-related filters. |
Filter Registration
Inside wpr_translate_bootstrap_plugin():
add_filter( 'load_textdomain_mofile', 'wpr_translate_override_mofile', 10, 2 );
add_filter( 'override_load_textdomain', 'wpr_translate_override_load_textdomain', 10, 3 );
add_filter( 'gettext', 'wpr_translate_filter_gettext', 10, 3 );
add_filter( 'gettext_with_context', 'wpr_translate_filter_gettext_with_context', 10, 4 );
add_filter( 'locale', 'wpr_translate_filter_frontend_locale', 5 );
Compilation: DB → MO
wpr_translate_admin_generate_translation_files() (in string-export.php) is the entry point. It:
- Loads wpr_translate_languages and resolves the default language code.
- Verifies the strings table exists and the processed column is present.
- Selects every row from {prefix}wpestate_translation_strings (non-empty translation only).
- Groups results by context then language_code.
- Maps each context to a text domain by re-running wpr_translate_admin_get_scan_targets() and reading $target[‘domain’]. If that lookup fails, the context slug is used as fallback.
- Calls wpr_translate_prepare_custom_translations_directory() to ensure the output directory exists and is writable.
- Uses WordPress’s MO and Translation_Entry classes (wp-includes/pomo/mo.php, entry.php) to build each MO file in memory:
- Imports an existing compiled MO from wpr_translate_locate_custom_mofile() so previous translations are retained.
- Sets headers: Project-Id-Version, POT-Creation-Date, PO-Revision-Date, Language, Content-Type: text/plain; charset=UTF-8, Plural-Forms: nplurals=2; plural=(n != 1);
- Adds entries with singular/translations.
- Writes via $mo->export_to_file( $file_path ) using wpr_translate_build_custom_mofile_path().
- Updates processed = 1 on exported rows in a single prepared UPDATE with IN() placeholders.
The return value is array( 'files' => int, 'strings' => int, 'errors' => string[] ).
Output File Naming
Built by wpr_translate_build_custom_mofile_path( $domain, $locale ):
- Domain passes through wpr_translate_normalize_translation_domain() (lowercased, non-[a-z0-9_\-.] replaced with -).
- Locale passes through wpr_translate_normalize_translation_locale() (language part lowercased, region/script parts uppercased, dashes converted to underscores).
- Final name: {directory}/{domain}-{locale}.mo (example: wpresidence-fr_FR.mo).
Output Directory Resolution
wpr_translate_get_custom_translations_directory_candidates() produces, in order:
- {stylesheet_directory}/languages/wpr — child theme first.
- {template_directory}/languages/wpr — parent theme next.
- {WPR_TRANSLATE_PLUGIN_ROOT}/languages/wpr — plugin fallback.
wpr_translate_select_preferred_translations_directory() prefers writable theme directories, then any other writable candidate. The chosen path is cached in $GLOBALS['wpr_translate_custom_directory'] and mirrored to the wpr_translate_custom_directory option. Two filters expose control:
- wpr_translate_custom_translations_directory_candidates — modify the candidate list.
- wpr_translate_custom_translations_directory — modify the final selection (receives
$cached_directory, $candidates).
Load-Order Override
When WordPress asks for a textdomain’s MO file, load_textdomain_mofile is filtered first. wpr_translate_override_mofile( $mofile, $domain ) substitutes the plugin-generated MO file (via wpr_translate_locate_custom_mofile()) when one exists. wpr_translate_override_load_textdomain( $override, $domain, $mofile ) then records the load in the global registry ($GLOBALS[‘wpr_translate_loaded_mofiles’]) so runtime code can tell whether a custom MO is active for a (domain, locale) pair.
The registry entry shape:
array(
'domain' => 'wpresidence',
'locale' => 'fr_FR',
'path' => '/abs/path/to/wpresidence-fr_FR.mo',
'source' => 'custom' | 'wordpress',
);
wpr_translate_has_custom_mofile_loaded( $domain, $locale ) returns true only when source === ‘custom’.
Runtime Gettext Filters
wpr_translate_filter_gettext() and wpr_translate_filter_gettext_with_context() (in translation-hooks.php) both delegate to wpr_translate_maybe_provide_translation( $translation, $text, $domain ) defined in translation-runtime.php.
That provider is intentionally restrictive:
- Skips when
$translation !== $text(something else already translated). - Calls wpr_translate_runtime_should_use_database_translations() which returns true only when the current request is the strings editor page (
$_GET['page'] === 'wpr-translate-strings'or the same key in$_POST). - Skips when a custom MO is already loaded for the domain/locale (wpr_translate_has_custom_mofile_loaded()).
- Skips when the domain has no DB rows for the active language (wpr_translate_domain_has_runtime_translations(), cached per request).
- Looks up the row via wpr_translate_lookup_runtime_translation() — indexed by
context IN (candidate contexts) AND name = 'str_' . md5($original) AND language_code = $code, with a LIKE fallback for legacy context values.
Net effect: front-end requests always go through normal gettext + compiled MO files. Database-backed translations are a live-preview layer that only kicks in while the translator is editing.
Active Locale Resolution
wpr_translate_get_active_locale() (recursion-safe via a static flag) follows this order:
- On admin non-AJAX requests → determine_locale() / get_locale().
- wpr_translate_get_current_language() (router-resolved language).
- wpr_translate_get_context_language() (admin-selected editing language).
- wpr_translate_get_default_language().
- WordPress fallback.
wpr_translate_get_active_language_code() mirrors the same chain for language codes.
Options Used
| Option | Purpose |
|---|---|
| wpr_translate_languages | Languages registry — read during compile. |
| wpr_translate_custom_directory | Cached output directory for generated MO files. |
| wpr_translate_scan_state | Scanner mtime + language signatures (not read at compile time). |
Hooks Summary
| Hook | Type | Callback / Purpose |
|---|---|---|
| load_textdomain_mofile | filter | wpr_translate_override_mofile — substitute generated MO. |
| override_load_textdomain | filter | wpr_translate_override_load_textdomain — record load outcome. |
| gettext | filter | wpr_translate_filter_gettext → runtime DB lookup (editor only). |
| gettext_with_context | filter | wpr_translate_filter_gettext_with_context (same logic, with context). |
| locale | filter | wpr_translate_filter_frontend_locale — align frontend locale with current language. |
| wpr_translate_runtime_translation | filter | Applied to DB-resolved replacement before return. |
| wpr_translate_custom_translations_directory | filter | Final output directory for generated MO files. |
| wpr_translate_custom_translations_directory_candidates | filter | Candidate directory list. |
Debug Hook
plugin-bootstrap.php also attaches wpr_debug_translations to gettext at priority 999 when debug logging is enabled. Use this when tracing why a string falls back to English.
Further Reading
- String Scanner — how rows land in the strings table in the first place.
- Translating Theme & Plugin Strings — the admin UI and bulk actions.
- Automatic Translation — the provider-level API integration.
See also our multi-language real estate website guide.