ForgeWP

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.ts alongside your theme metadata.
  • Author your translation dictionary in cms/translations.ts (or cms/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.
Flowchart
Rendering diagram...

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:

wp.config.ts
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

Table · 4 columns
Option
Type
Required
Description
locales
string[]
Yes
List of ISO 639-1 two-letter language codes supported by your theme (e.g. ['en', 'de', 'fr']).
defaultLocale
string
Yes
The primary source language used in your React components and fallback catalogs (e.g. 'en').
provider
string
No
CLI translation provider: 'local' (or 'manual'), 'deepl', 'google', or 'libretranslate'. Defaults to 'local'.
providerConfig
object
No
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:

Generated WordPress PHP Output
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:

functions.php (Generated Polylang integration)
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:

  1. Enables language assignment and translation metaboxes for custom post types in the WordPress admin.
  2. Filters WordPress REST API queries by language parameter (?lang=de) so that useWpQuery fetches only posts in the active language.
  3. 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

Project directory 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 domain

Authoring 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:

cms/translations.ts
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 translations

TypeScript 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

src/components/HeroSection.tsx
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:

src/components/NewsletterForm.tsx
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():

Correct: Context-aware hook invocation
1import { useWpI18n } from "@forgewp/react"
2
3export function FeatureBadge() {
4  const { __ } = useWpI18n()
5  return <span>{__("New Season")}</span>
6}
Incorrect: Standalone function import
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:

  1. At build time (Server-side rendering): Emits unique compiler tokens for static PHP compilation.
  2. In local Vite preview: Reads synchronous translations from cms/translations.ts.
  3. 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:

Compiler token emission
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:

Before Markup Processing (Compiler Intermediate)
1<h1>__FORGEWP_I18N_Experience Alpine Elegance__</h1>
After Markup Processing (Final theme PHP template)
1<h1><?php echo __('Experience Alpine Elegance', 'alpine-resort'); ?></h1>

HTML entities, quotes, and special characters are automatically normalized and escaped (replace(/&#39;/g, "'"), replace(/&quot;/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:

Generated in functions.php (Hydration Enqueuer)
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:

Flowchart
Rendering diagram...
  1. Polylang Active: Resolves language via pll_current_language().
  2. WPML Active: Resolves language via the ICL_LANGUAGE_CODE constant.
  3. URL Path Matching: When using local URL prefixes (such as /de/ or /fr/), the compiler matches the first URL segment against wp.config.ts's configured locales.
  4. Core WordPress Fallback: Falls back to WordPress core's get_locale() (e.g., de_DE is sliced to de), or wp.config.ts's defaultLocale.

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:

Generated in functions.php
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:

Generated in functions.php
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.

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:

Generated in functions.php
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:

Table · 3 columns
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.locales and i18n.defaultLocale in wp.config.ts.
  • You have specified your theme's textDomain in wp.config.ts.
  • Your custom post types have translatable: true if you plan to translate them with Polylang.
  • Your components import useWpI18n from @forgewp/react and call const { __ } = useWpI18n().
  • Your translation dictionary in cms/translations.ts is wrapped with defineTranslations().

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.