Multilingual Setup¶
This guide covers the Polylang Pro integration, language configuration, hreflang tags, AI-powered translation, and language-aware REST API queries.
Language structure¶
The site launches in two languages with English as the default:
| Language | Locale | URL structure | Flag |
|---|---|---|---|
| English (default) | en_US |
Root path (/) |
us |
| Deutsch | de_DE |
Subdirectory (/de/) |
de |
The default language (English) uses hide_default: true, meaning English URLs have no language prefix. German content lives under /de/. This is configured programmatically -- not through the Polylang admin UI.
Code-defined configuration¶
All Polylang settings are managed in web/app/mu-plugins/polylang-customizations.php and synced to the database on every admin load. This means the configuration is version-controlled and consistent across environments.
Language creation¶
The tinnitus_polylang_ensure_languages() function runs on the pll_init hook and creates languages if they do not exist:
$desired = [
[
'name' => 'English',
'slug' => 'en',
'locale' => 'en_US',
'rtl' => false,
'term_group' => 0,
'flag' => 'us',
],
[
'name' => 'Deutsch',
'slug' => 'de',
'locale' => 'de_DE',
'rtl' => false,
'term_group' => 1,
'flag' => 'de',
],
];
Options sync¶
The tinnitus_polylang_sync_options() function enforces these Polylang options:
$desired = [
'default_lang' => 'en',
'force_lang' => 1, // Subdirectory URL structure
'hide_default' => true, // EN at root, DE at /de/
'rewrite' => true, // Remove /language/ from URLs
'browser' => false, // Don't auto-detect browser language
'redirect_lang' => false, // Don't redirect based on browser language
'post_types' => ['article', 'research', 'landing'],
'taxonomies' => ['cornerstone', 'tinnitus_type', 'tinnitus_stage', 'treatment_modality', 'audience'],
];
Adding a new translatable post type or taxonomy
When you create a new CPT or taxonomy that should be multilingual, add it to the post_types or taxonomies array in this function. The change deploys with the code -- no manual Polylang admin configuration needed.
Orphaned content assignment¶
On every admin load, tinnitus_polylang_assign_orphaned_content() checks all posts and taxonomy terms for a missing language assignment and assigns them to the default language (English). This handles post-migration scenarios and content created before Polylang was active.
Hreflang tags¶
Polylang generates <link rel="alternate" hreflang="..."> tags automatically. However, it omits the x-default tag when hide_default is enabled. The project adds it back via a filter:
function tinnitus_add_xdefault_hreflang(array $hreflangs): array
{
if (isset($hreflangs['en']) && ! isset($hreflangs['x-default'])) {
$hreflangs['x-default'] = $hreflangs['en'];
}
return $hreflangs;
}
add_filter('pll_rel_hreflang_attributes', 'tinnitus_add_xdefault_hreflang');
This produces correct hreflang output for SEO:
<link rel="alternate" hreflang="en" href="https://naluma.app/article-slug/" />
<link rel="alternate" hreflang="de" href="https://naluma.app/de/article-slug/" />
<link rel="alternate" hreflang="x-default" href="https://naluma.app/article-slug/" />
Localized date formats¶
WordPress stores a single date_format option. The project overrides it per language via the option_date_format filter so dates display in the correct locale format:
| Language | Format | Example |
|---|---|---|
| English | F j, Y |
March 26, 2026 |
| German | j. F Y |
26. Marz 2026 |
String translations¶
The theme uses esc_html__('...', 'tinnitus') for all translatable strings. Polylang stores string translations in the database under the pll_string text domain. A bridge function copies the Polylang MO data into the tinnitus domain so both resolve:
function tinnitus_polylang_load_string_translations(): void
{
if (isset($GLOBALS['l10n']['pll_string']) && $GLOBALS['l10n']['pll_string'] instanceof PLL_MO) {
$GLOBALS['l10n']['tinnitus'] = &$GLOBALS['l10n']['pll_string'];
}
}
add_action('pll_language_defined', 'tinnitus_polylang_load_string_translations', 10);
This means you manage string translations through Polylang's "Strings translations" admin page, and they automatically apply to all __('...', 'tinnitus') calls in theme code.
AI translation plugin¶
The tinnitus-ai-translate mu-plugin provides automated translation via OpenAI or Anthropic (Claude) APIs. It lives at web/app/mu-plugins/tinnitus-ai-translate/.
Capabilities¶
- Translates post content (title, body, excerpt) including block markup
- Translates ACF field values (summary, FAQ, sources)
- Translates taxonomy terms
- Translates Polylang registered strings
- Automatic theme string discovery -- scans PHP files for
__()andesc_html__()calls and registers them with Polylang
Prerequisites¶
- Polylang Pro must be active (the plugin checks for
POLYLANG_VERSIONon load) - Action Scheduler must be available for auto-translation on publish (provided by Rank Math or WooCommerce)
- An API key for OpenAI or Anthropic must be configured in the plugin settings
Admin interface¶
- Settings page: WP Admin menu item for configuring the AI provider and API key
- Metabox: Each translatable post shows a translation metabox with a button to trigger AI translation
- Auto-translation: When a post is published, the plugin schedules an Action Scheduler job to translate it automatically
Action Scheduler dependency
If Action Scheduler is not available, the plugin displays an admin notice and disables auto-translation on publish. Manual translation via the metabox still works.
Language-aware REST API¶
The custom REST API at /tinnitus/v1/ supports a lang parameter for filtering content by language. This is consumed by the Flutter app and the marketing automation pipeline.
Query parameter¶
Pass lang=en or lang=de to filter results:
The lang parameter is declared with validation in the base controller's get_collection_params():
'lang' => [
'type' => 'string',
'enum' => ['en', 'de'],
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
'description' => 'Filter by language (requires Polylang).',
],
Response fields¶
When Polylang is active, each item in the REST response includes language metadata:
{
"id": 42,
"title": "Understanding Tinnitus",
"lang": "en",
"translations": {
"en": 42,
"de": 87
}
}
The translations object maps language slugs to post IDs, allowing the client to fetch the translated version directly.
Query internals¶
The language filter is applied in build_query_args() on the base controller. When Polylang is active, passing 'lang' => 'de' to WP_Query triggers Polylang's query filter, which restricts results to posts assigned to that language:
$lang = $request->get_param('lang');
if (is_string($lang) && '' !== $lang) {
$args['lang'] = $lang;
}
Adding a new language¶
To add a third language in the future:
- Add the language definition to the
$desiredarray intinnitus_polylang_ensure_languages() - Add the language slug to the
langenum in the REST API controller (get_collection_params()) - Add a date format entry to
tinnitus_localize_date_format()if the locale has different date conventions - Deploy the code -- the language is created automatically on the next admin page load
- Translate existing content via the AI translation plugin or manually through Polylang's translation workflow