Configure Locales and Translations
Declare theme languages, author the local translation catalog, use the useWpI18n hook, and understand compile-time gettext code generation with runtime Polylang and WPML integration.
Building multilingual WordPress themes has historically been complex and error-prone. Traditional workflows forced developers to extract string literals into separate .po and .mo binary catalogs using external desktop tools like Poedit, juggle WordPress PHP gettext functions (__(), _e(), _x()), register separate script translation handles in PHP (wp_set_script_translations), and coordinate language plugins manually.
ForgeWP introduces a unified, zero-overhead internationalization (i18n) architecture designed specifically for modern React theme development:
- Declare supported locales in
wp.config.tsalongside your theme metadata. - Author your translation dictionary in
cms/translations.ts(orcms/translations.json) using type-safe helpers. - Translate strings in React components using the
useWpI18n()hook and its returned__()function. - Generate native WordPress gettext calls automatically during compilation, producing clean PHP
<?php echo __('Text', 'theme-slug'); ?>with zero client-side JavaScript overhead. - Seamless runtime integration with Polylang, WPML, and native WordPress locales during client hydration.
Declare Supported Locales in wp.config.ts
Internationalization is configured in your theme root configuration file, wp.config.ts. The i18n block defines which language codes your theme supports, which locale serves as the default fallback, and which translation provider the CLI will use for automated workflows.
Add the i18n property to your defineConfig call:
1import { defineConfig } from "@forgewp/compiler/define-config"
2import { siteOptions } from "./cms/site-options"
3import { themeMods } from "./cms/theme-mods"
4
5export default defineConfig({
6 name: "Alpine Resort",
7 slug: "alpine-resort",
8 version: "1.0.0",
9 textDomain: "alpine-resort",
10
11 // Internationalization configuration
12 i18n: {
13 locales: ["en", "de", "fr", "it"],
14 defaultLocale: "en",
15 provider: "local",
16 },
17
18 // Translatable Custom Post Types
19 postTypes: {
20 room: {
21 translatable: true,
22 labels: {
23 singular: "Room & Suite",
24 plural: "Rooms & Suites",
25 },
26 },
27 activity: {
28 translatable: true,
29 labels: {
30 singular: "Activity",
31 plural: "Activities",
32 },
33 },
34 },
35
36 options: siteOptions,
37 themeMods: themeMods,
38})Configuration Options
Option Type Required Description localesstring[]Yes List of ISO 639-1 two-letter language codes supported by your theme (e.g. ['en', 'de', 'fr']).defaultLocalestringYes The primary source language used in your React components and fallback catalogs (e.g. 'en').providerstringNo CLI translation provider: 'local' (or 'manual'), 'deepl', 'google', or 'libretranslate'. Defaults to 'local'.providerConfigobjectNo Provider-specific settings, such as apiKey or self-hosted API endpoint. |
|---|
Binding the WordPress Text Domain
The textDomain property declared at the root of wp.config.ts binds directly to all generated WordPress translation calls:
1<?php echo __('Discover our luxury alpine chalets.', 'alpine-resort'); ?>ForgeWP uses the configured textDomain for recognized useWpI18n() translations. Keep that domain consistent with your translation files and extraction workflow. Verify translated strings with your chosen plugin or language-pack workflow before release.
Translatable Custom Post Types and Taxonomies
When you register custom post types in wp.config.ts, adding translatable: true instructs ForgeWP to automatically generate Polylang integration hooks in your theme's functions.php:
1// Automatically generated in functions.php
2add_filter('pll_get_post_types', function($post_types, $is_settings) {
3 $post_types['room'] = 'room';
4 $post_types['activity'] = 'activity';
5 return $post_types;
6}, 10, 2);
7
8add_filter('rest_room_query', 'forgewp_rest_query_language_filter', 10, 2);
9add_filter('rest_activity_query', 'forgewp_rest_query_language_filter', 10, 2);This integration automatically:
- Enables language assignment and translation metaboxes for custom post types in the WordPress admin.
- Filters WordPress REST API queries by language parameter (
?lang=de) so thatuseWpQueryfetches only posts in the active language. - Automatically registers all custom taxonomies with Polylang via
pll_get_taxonomies.
Author the Translation Catalog
ForgeWP stores theme translation catalogs inside the cms/ directory. You can author your catalog using either TypeScript (cms/translations.ts) or JSON (cms/translations.json).
Catalog Location and File Anatomy
my-theme/
├── cms/
│ ├── translations.ts <-- Primary translation catalog
│ ├── site-options.ts
│ └── mock-data.ts
├── src/
│ ├── components/
│ │ ├── Header.tsx
│ │ └── Hero.tsx
│ └── app/
│ └── pages/
│ └── FrontPage.tsx
└── wp.config.ts <-- Declares locales and text domainAuthoring cms/translations.ts with defineTranslations()
The recommended format is TypeScript using the defineTranslations() helper from @forgewp/react. It provides syntax highlighting, auto-completion, and strict validation:
1import { defineTranslations } from "@forgewp/react"
2
3export const translations = defineTranslations({
4 en: {
5 "Discover Luxury": "Discover Luxury",
6 "Book Your Stay": "Book Your Stay",
7 "Explore Suites": "Explore Suites",
8 "From $320 per night": "From $320 per night",
9 "Contact Concierge": "Contact Concierge",
10 "All rights reserved.": "All rights reserved.",
11 },
12 de: {
13 "Discover Luxury": "Luxus entdecken",
14 "Book Your Stay": "Aufenthalt buchen",
15 "Explore Suites": "Suiten entdecken",
16 "From $320 per night": "Ab 320 $ pro Nacht",
17 "Contact Concierge": "Concierge kontaktieren",
18 "All rights reserved.": "Alle Rechte vorbehalten.",
19 },
20 fr: {
21 "Discover Luxury": "Decouvrez le luxe",
22 "Book Your Stay": "Reservez votre sejour",
23 "Explore Suites": "Explorer les suites",
24 "From $320 per night": "A partir de 320 $ par nuit",
25 "Contact Concierge": "Contacter la conciergerie",
26 "All rights reserved.": "Tous droits reserves.",
27 },
28 it: {
29 "Discover Luxury": "Scopri il lusso",
30 "Book Your Stay": "Prenota il tuo soggiorno",
31 "Explore Suites": "Esplora le suite",
32 "From $320 per night": "Da 320 $ a notte",
33 "Contact Concierge": "Contatta il concierge",
34 "All rights reserved.": "Tutti i diritti riservati.",
35 },
36})
37
38export default translationsTypeScript versus JSON Catalogs
ForgeWP supports both catalog file formats:
cms/translations.ts(Recommended): Best for developer workflows where comments, type checking, and modular organization are valuable.cms/translations.json: Best when third-party translation tools, external localization platforms, or non-technical copywriters edit translation files directly.
Note
Precedence Rule: If both cms/translations.ts and cms/translations.json exist in your theme root, the TypeScript file (cms/translations.ts) takes precedence during compilation and development.
Translate Strings in Components with useWpI18n()
To translate text inside your React components, import the useWpI18n() hook from @forgewp/react. The hook returns an object containing the translation function __.
Basic Component Translation
1import { useWpI18n } from "@forgewp/react"
2
3export function HeroSection() {
4 const { __ } = useWpI18n()
5
6 return (
7 <section className="relative overflow-hidden bg-slate-900 py-24 text-white">
8 <div className="mx-auto max-w-5xl px-6 text-center">
9 <span className="text-xs font-semibold uppercase tracking-widest text-emerald-400">
10 {__("Discover Luxury")}
11 </span>
12 <h1 className="mt-4 text-4xl font-extrabold tracking-tight sm:text-6xl">
13 {__("Experience Alpine Elegance")}
14 </h1>
15 <p className="mx-auto mt-6 max-w-2xl text-lg text-slate-300">
16 {__("Surround yourself with breathtaking peaks, world-class amenities, and tailored mountain hospitality.")}
17 </p>
18 <div className="mt-10 flex items-center justify-center gap-4">
19 <a
20 href="/booking"
21 className="rounded-lg bg-emerald-500 px-6 py-3 font-medium text-white transition hover:bg-emerald-400"
22 >
23 {__("Book Your Stay")}
24 </a>
25 <a
26 href="/suites"
27 className="rounded-lg border border-slate-700 px-6 py-3 font-medium text-slate-200 transition hover:bg-slate-800"
28 >
29 {__("Explore Suites")}
30 </a>
31 </div>
32 </div>
33 </section>
34 )
35}Translating Component Attributes and Props
The __() function returns a translated string, making it safe to use in JSX element children, HTML attributes, and component props:
1import { useWpI18n } from "@forgewp/react"
2
3export function NewsletterForm() {
4 const { __ } = useWpI18n()
5
6 return (
7 <form className="flex flex-col gap-3 sm:flex-row">
8 <input
9 type="email"
10 placeholder={__("Enter your email address")}
11 aria-label={__("Email subscription")}
12 required
13 className="rounded-md border border-slate-300 px-4 py-2 text-sm focus:border-emerald-500 focus:outline-none"
14 />
15 <button
16 type="submit"
17 className="rounded-md bg-emerald-600 px-5 py-2 text-sm font-semibold text-white hover:bg-emerald-500"
18 >
19 {__("Subscribe")}
20 </button>
21 </form>
22 )
23}Crucial Best Practice: Do Not Import __ Directly
Always access __ through useWpI18n():
1import { useWpI18n } from "@forgewp/react"
2
3export function FeatureBadge() {
4 const { __ } = useWpI18n()
5 return <span>{__("New Season")}</span>
6}1// Never do this:
2// import { __ } from "@forgewp/react"Invoking const { __ } = useWpI18n() enables the ForgeWP compiler and runtime to swap the implementation automatically based on execution context:
- At build time (Server-side rendering): Emits unique compiler tokens for static PHP compilation.
- In local Vite preview: Reads synchronous translations from
cms/translations.ts. - During client hydration: Reads synchronous translations from
window.forgeWpTranslations.
How Compilation and SSR Work Under the Hood
One of ForgeWP's biggest advantages over headless frameworks is its zero-overhead output. Internationalized text does not require client-side JavaScript execution or runtime API calls to render.
1. Compile-Time AST Extraction and Token Generation
During the build step (pnpm forgewp export), ForgeWP's Node SSR engine renders your React component tree. When useWpI18n() is invoked in Node, it returns a special compiler token:
1__: (text: string) => `__FORGEWP_I18N_${text}__`2. Markup Processor Transformation to Native WordPress PHP
Next, the compiler's markup processor scans all generated HTML templates. It detects the token and converts it directly into native WordPress PHP gettext function calls:
1<h1>__FORGEWP_I18N_Experience Alpine Elegance__</h1>1<h1><?php echo __('Experience Alpine Elegance', 'alpine-resort'); ?></h1>HTML entities, quotes, and special characters are automatically normalized and escaped (replace(/'/g, "'"), replace(/"/g, '"')) so that valid, secure PHP is generated every time.
3. Server-Safe Hook Classification
The ForgeWP compiler classifies useWpI18n as a server-safe hook in SERVER_SAFE_HOOKS.
If a component uses useWpI18n to translate static text and contains no interactive event handlers (like onClick or onChange), ForgeWP leaves the component as 100% static PHP markup. No client hydration island is created, and 0 KB of client JavaScript is shipped for that component.
4. Interactive Islands and Client Hydration
When a component is interactive (for instance, an interactive modal, filter bar, or language switcher), it requires selective hydration. To ensure translations work synchronously in the browser without network requests, ForgeWP's hydration-enqueuer.js automatically injects the compiled translation catalog into the page runtime:
1wp_add_inline_script(
2 'alpine-resort-react-runtime',
3 'window.forgeWpTranslations = ' . wp_json_encode(array(
4 'currentLanguage' => $current_lang,
5 'urls' => $translations,
6 'homeUrls' => $home_urls,
7 'translations' => $translated_dict
8 ), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE) . ';',
9 'before'
10);When hydrated components call useWpI18n() in the browser, lookups resolve instantly from window.forgeWpTranslations.translations[text]. There are no layout shifts, no flickering, and no asynchronous fetch delays.
WordPress Ecosystem Integration: Polylang, WPML, and Core Locales
ForgeWP bridges React components with WordPress multilingual plugins without requiring custom PHP glue code.
Runtime Language Resolution Hierarchy
When a visitor requests a page on your WordPress site, ForgeWP determines the active language using an intelligent cascade:
- Polylang Active: Resolves language via
pll_current_language(). - WPML Active: Resolves language via the
ICL_LANGUAGE_CODEconstant. - URL Path Matching: When using local URL prefixes (such as
/de/or/fr/), the compiler matches the first URL segment againstwp.config.ts's configuredlocales. - Core WordPress Fallback: Falls back to WordPress core's
get_locale()(e.g.,de_DEis sliced tode), orwp.config.ts'sdefaultLocale.
Automatic Polylang String Registration
During the theme export process, ForgeWP scans all template files for unique translation keys. In functions.php, it registers each extracted key with Polylang:
1$i18n_keys = array(
2 'Discover Luxury',
3 'Book Your Stay',
4 'Explore Suites',
5 'Contact Concierge'
6);
7
8if (function_exists('pll_register_string')) {
9 foreach ($i18n_keys as $key) {
10 pll_register_string($key, $key, 'alpine-resort');
11 }
12}This feature allows non-technical site administrators to open Languages → String Translations in the WordPress admin dashboard and customize translations directly within WordPress without touching source code.
Language-Aware Front Page and Route Resolution
On multilingual WordPress sites running Polylang or WPML, each language has its own independent front page post (for example, Post ID 12 for English, Post ID 45 for German).
ForgeWP automatically generates language-aware resolution helpers:
1function forgewp_resolve_front_page_id() {
2 $anchor_id = (int) get_option('page_on_front');
3 if ($anchor_id <= 0) {
4 return 0;
5 }
6 if (function_exists('pll_get_post')) {
7 $current_lang = function_exists('pll_current_language') ? pll_current_language() : '';
8 if (!empty($current_lang)) {
9 $translated_id = pll_get_post($anchor_id, $current_lang);
10 if ($translated_id > 0) {
11 return $translated_id;
12 }
13 }
14 }
15 return $anchor_id;
16}Whether resolving Gutenberg blocks, page metadata, or custom fields, ForgeWP resolves the exact post translation corresponding to the active visitor language.
Multilingual Navigation Menus
Navigation menus configured with cms/menus.ts or managed in Appearance → Menus automatically adjust to the active language.
Menu Location Naming Convention
ForgeWP supports both base menu locations and language-specific location identifiers:
- Base Location:
primary,footer - Language Location:
primary___de,primary___fr,footer___it
In functions.php, ForgeWP generates the forgewp_get_menu_id_for_lang() resolver:
1function forgewp_get_menu_id_for_lang($location, $lang = '') {
2 $locations = get_nav_menu_locations();
3 if (empty($locations) || !is_array($locations)) {
4 return null;
5 }
6
7 // 1. Check for language-specific location (e.g. primary___de)
8 if (!empty($lang)) {
9 $lang_key = $location . '___' . $lang;
10 if (isset($locations[$lang_key])) {
11 return $locations[$lang_key];
12 }
13 }
14
15 // 2. Check for base location (e.g. primary)
16 if (isset($locations[$location])) {
17 return $locations[$location];
18 }
19
20 // 3. Fallback to any matching key in locations
21 foreach ($locations as $key => $val) {
22 if ($key === $location || strpos($key, $location . '___') === 0) {
23 return $val;
24 }
25 }
26 return null;
27}When you render navigation menus using useWpMenu() or <WpMenu />, the component automatically receives the menu tree translated for the current language.
Local Development Preview versus Production Export
ForgeWP provides consistent internationalization behavior between local Vite development and production WordPress hosting:
Feature Local Vite Preview ( pnpm dev)Production WordPress Theme Catalog Source cms/translations.ts or translations.json loaded into window._forgeWpMockTranslations.translations.json loaded via PHP wp_add_inline_script and gettext .mo files.Active Locale Detected from URL path prefix (e.g. http://localhost:3000/de/) or defaults to defaultLocale.Resolved via Polylang ( pll_current_language), WPML, or WordPress core get_locale().Static Markup Rendered via React DOM using active dictionary lookup. Native PHP gettext: <?php echo __('Text', 'theme'); ?>.String Overrides Live reload when cms/translations.ts is edited.Editable in WordPress admin (Languages → String Translations). Performance Overhead Minimal (instant in-memory dictionary lookup). Zero (compiled PHP execution; 0 KB client JS for static components). |
|---|
Summary Checklist
Before proceeding to the automated extraction and translation workflows in the next chapter, verify that:
- You have declared
i18n.localesandi18n.defaultLocaleinwp.config.ts. - You have specified your theme's
textDomaininwp.config.ts. - Your custom post types have
translatable: trueif you plan to translate them with Polylang. - Your components import
useWpI18nfrom@forgewp/reactand callconst { __ } = useWpI18n(). - Your translation dictionary in
cms/translations.tsis wrapped withdefineTranslations().
Next Steps
Now that your theme is configured for internationalization, proceed to Extract, Translate, and Switch Languages to learn how to use the ForgeWP CLI to extract string keys, connect automated translation engines (DeepL, Google, LibreTranslate), and build interactive language switchers.