ワンクリックで
wp-bilingual
Bilingual/multilingual i18n methodology using ACF _suffix pattern with transparent translation helpers
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Bilingual/multilingual i18n methodology using ACF _suffix pattern with transparent translation helpers
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
WordPress legacy theme best practices — proper enqueueing, escaping, hooks, security, and performance
Rank Math SEO configuration reference, schema JSON-LD templates, meta patterns, and SEO seeding commands
Audit criteria, severity definitions, report JSON schema, and quality thresholds for wp-audit agents
WP-CLI-first principle — use WP-CLI instead of generating PHP code whenever possible, with command reference, ACF seeding patterns, and environment-aware execution
Environment detection, configuration, and WP-CLI wrapper selection for local WordPress development
Demo HTML creation methodology — single-file demos with section comments for 1:1 WordPress conversion
| name | wp-bilingual |
| description | Bilingual/multilingual i18n methodology using ACF _suffix pattern with transparent translation helpers |
| user-invocable | false |
This skill defines the translation methodology for WordPress themes that need to support multiple languages. It uses the ACF/SCF _suffix pattern -- no WPML, no Polylang, no separate pages per language. One set of pages, one set of fields, with suffixed duplicates for secondary languages.
For every translatable ACF/SCF field, the primary language (typically English) uses the base field name. Each secondary language gets a duplicate field with a language suffix appended.
| Primary Field (EN) | Spanish Field | French Field |
|---|---|---|
hero_title | hero_title_es | hero_title_fr |
hero_subtitle | hero_subtitle_es | hero_subtitle_fr |
cta_button_text | cta_button_text_es | cta_button_text_fr |
service_description | service_description_es | service_description_fr |
_<lang> suffix and are optional (fall back to primary if empty)Define supported languages and the default at the top of inc/i18n.php.
// Define supported languages
define('PREFIX_SUPPORTED_LANGS', array('en', 'es'));
define('PREFIX_DEFAULT_LANG', 'en');
Language is detected using a strict priority chain. The first match wins.
Priority: URL parameter > Cookie > Browser Accept-Language > Default
function prefix_get_current_lang() {
static $current_lang = null;
if ($current_lang !== null) {
return $current_lang;
}
// 1. Check URL parameter
if (isset($_GET['lang']) && in_array($_GET['lang'], PREFIX_SUPPORTED_LANGS)) {
$current_lang = sanitize_text_field($_GET['lang']);
// Set cookie for persistence (365 days)
setcookie('prefix_lang', $current_lang, time() + (365 * 24 * 60 * 60), '/');
return $current_lang;
}
// 2. Check cookie
if (isset($_COOKIE['prefix_lang']) && in_array($_COOKIE['prefix_lang'], PREFIX_SUPPORTED_LANGS)) {
$current_lang = sanitize_text_field($_COOKIE['prefix_lang']);
return $current_lang;
}
// 3. Check browser language (Accept-Language header)
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$browser_lang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2);
if (in_array($browser_lang, PREFIX_SUPPORTED_LANGS)) {
$current_lang = $browser_lang;
return $current_lang;
}
}
// 4. Default language
$current_lang = PREFIX_DEFAULT_LANG;
return $current_lang;
}
setcookie() MUST be called before any HTML output (before headers are sent)i18n.php file must be included early in functions.php, before any template rendering/ so it works across all pagesWhen the user clicks a language switcher link (e.g., ?lang=es), the cookie is set in the prefix_get_current_lang() function. Subsequent page loads read the cookie, so the URL parameter is only needed once.
// Cookie is set when URL param is detected
setcookie('prefix_lang', $current_lang, time() + (365 * 24 * 60 * 60), '/');
This is the primary function for retrieving any ACF/SCF field. It checks the current language, tries the suffixed field first, and falls back to the primary field.
function prefix_get_field($field_name, $post_id = null) {
$lang = prefix_get_current_lang();
// If secondary language, try suffixed field first
if ($lang !== PREFIX_DEFAULT_LANG) {
$translated_field = $field_name . '_' . $lang;
$value = get_field($translated_field, $post_id);
// If translated field has a value, return it
if (!empty($value)) {
return $value;
}
}
// Fallback to primary (default) field
return get_field($field_name, $post_id);
}
Usage in templates:
<h1><?php echo esc_html(prefix_get_field('hero_title')); ?></h1>
<p><?php echo wp_kses_post(prefix_get_field('hero_description')); ?></p>
<!-- With post ID -->
<?php $logo = prefix_get_field('site_logo', 'option'); ?>
<!-- With specific post -->
<?php $title = prefix_get_field('custom_title', $post->ID); ?>
Translates specific subfields within a repeater while leaving non-translatable subfields (images, URLs) untouched.
function prefix_get_repeater($field_name, $translatable_subfields = array(), $post_id = null) {
$lang = prefix_get_current_lang();
$repeater = get_field($field_name, $post_id);
if (!$repeater || !is_array($repeater)) {
return array();
}
// If default language or no translatable subfields, return as-is
if ($lang === PREFIX_DEFAULT_LANG || empty($translatable_subfields)) {
return $repeater;
}
// Process each row for translations
foreach ($repeater as $index => $row) {
foreach ($translatable_subfields as $subfield) {
$translated_key = $subfield . '_' . $lang;
// If translated subfield exists and has value, override the primary
if (isset($row[$translated_key]) && !empty($row[$translated_key])) {
$repeater[$index][$subfield] = $row[$translated_key];
}
}
}
return $repeater;
}
Usage:
// Services repeater: translate 'title' and 'description', keep 'icon' and 'link' as-is
$services = prefix_get_repeater('services', array('title', 'description'));
foreach ($services as $service) : ?>
<div class="service-card">
<img src="<?php echo esc_url($service['icon']['url']); ?>" alt="">
<h3><?php echo esc_html($service['title']); ?></h3>
<p><?php echo esc_html($service['description']); ?></p>
</div>
<?php endforeach;
Used inside have_rows() loops (repeaters, flexible content) to get translated subfield values.
function prefix_get_sub_field($field_name) {
$lang = prefix_get_current_lang();
if ($lang !== PREFIX_DEFAULT_LANG) {
$value = get_sub_field($field_name . '_' . $lang);
if (!empty($value)) {
return $value;
}
}
return get_sub_field($field_name);
}
Usage inside have_rows():
<?php if (have_rows('team_members')) : ?>
<?php while (have_rows('team_members')) : the_row(); ?>
<div class="team-member">
<h3><?php echo esc_html(prefix_get_sub_field('name')); ?></h3>
<p><?php echo esc_html(prefix_get_sub_field('bio')); ?></p>
</div>
<?php endwhile; ?>
<?php endif; ?>
For hardcoded UI strings (navigation labels, button text, form labels) that do not come from ACF fields.
/**
* Get static translation string (return)
*/
function prefix__($key) {
$lang = prefix_get_current_lang();
$translations = prefix_get_translations();
if (isset($translations[$key][$lang])) {
return $translations[$key][$lang];
}
// Fallback to default language
if (isset($translations[$key][PREFIX_DEFAULT_LANG])) {
return $translations[$key][PREFIX_DEFAULT_LANG];
}
// Return key if translation not found
return $key;
}
/**
* Echo static translation string (with escaping)
*/
function prefix_e($key) {
echo esc_html(prefix__($key));
}
Usage:
<!-- In templates -->
<a href="#services"><?php prefix_e('nav_services'); ?></a>
<button><?php prefix_e('btn_learn_more'); ?></button>
<!-- When you need the raw string (e.g., for attributes) -->
<a href="#" aria-label="<?php echo esc_attr(prefix__('nav_schedule')); ?>">
Convenience helpers for language checks.
/**
* Check if current language matches
*/
function prefix_is_lang($lang) {
return prefix_get_current_lang() === $lang;
}
/**
* Alias: check if current language is Spanish
*/
function prefix_is_spanish() {
return prefix_get_current_lang() === 'es';
}
Usage:
<?php if (prefix_is_spanish()) : ?>
<html lang="es">
<?php else : ?>
<html lang="en">
<?php endif; ?>
Define all hardcoded UI strings in a central translations function. Each entry is an associative array keyed by language code.
function prefix_get_translations() {
return array(
// Navigation
'nav_home' => array(
'en' => 'Home',
'es' => 'Inicio',
),
'nav_services' => array(
'en' => 'Services',
'es' => 'Servicios',
),
'nav_pricing' => array(
'en' => 'Pricing',
'es' => 'Precios',
),
'nav_contact' => array(
'en' => 'Contact',
'es' => 'Contacto',
),
// Buttons
'btn_learn_more' => array(
'en' => 'Learn More',
'es' => 'Saber Mas',
),
'btn_get_started' => array(
'en' => 'Get Started',
'es' => 'Comenzar',
),
'btn_schedule' => array(
'en' => 'Schedule Appointment',
'es' => 'Agendar Cita',
),
// Footer
'footer_services' => array(
'en' => 'Services',
'es' => 'Servicios',
),
'footer_quick_links' => array(
'en' => 'Quick Links',
'es' => 'Enlaces Rapidos',
),
'footer_privacy' => array(
'en' => 'Privacy Policy',
'es' => 'Politica de Privacidad',
),
'footer_terms' => array(
'en' => 'Terms & Conditions',
'es' => 'Terminos y Condiciones',
),
// Social
'social_follow_us' => array(
'en' => 'Follow Us',
'es' => 'Siguenos',
),
);
}
For strings needed in client-side JS, create a filtered subset and pass via wp_localize_script().
function prefix_get_js_translations() {
$all = prefix_get_translations();
$lang = prefix_get_current_lang();
$js_strings = array();
// Pick only the keys needed in JS
$js_keys = array('btn_learn_more', 'btn_schedule', 'calc_per_month');
foreach ($js_keys as $key) {
if (isset($all[$key][$lang])) {
$js_strings[$key] = $all[$key][$lang];
}
}
return $js_strings;
}
Use remove_query_arg() and add_query_arg() to build language toggle URLs.
function prefix_get_lang_url($lang) {
$url = remove_query_arg('lang');
return add_query_arg('lang', $lang, $url);
}
Language switcher in a template:
<div class="lang-switcher">
<?php $current_lang = prefix_get_current_lang(); ?>
<?php foreach (PREFIX_SUPPORTED_LANGS as $lang) : ?>
<?php if ($lang !== $current_lang) : ?>
<a href="<?php echo esc_url(prefix_get_lang_url($lang)); ?>"
class="lang-switcher__link"
aria-label="<?php echo esc_attr('Switch to ' . strtoupper($lang)); ?>">
<?php echo esc_html(strtoupper($lang)); ?>
</a>
<?php endif; ?>
<?php endforeach; ?>
</div>
Register separate menu locations for each language. This allows admins to create fully localized menus in wp-admin.
function prefix_setup() {
register_nav_menus(array(
'primary-en' => __('Primary Navigation (EN)', 'theme-slug'),
'primary-es' => __('Primary Navigation (ES)', 'theme-slug'),
'mobile-en' => __('Mobile Navigation (EN)', 'theme-slug'),
'mobile-es' => __('Mobile Navigation (ES)', 'theme-slug'),
'footer-en' => __('Footer Navigation (EN)', 'theme-slug'),
'footer-es' => __('Footer Navigation (ES)', 'theme-slug'),
));
}
add_action('after_setup_theme', 'prefix_setup');
Select the menu location dynamically based on the current language.
<?php
$lang = prefix_get_current_lang();
wp_nav_menu(array(
'theme_location' => 'primary-' . $lang,
'container' => false,
'fallback_cb' => 'prefix_nav_fallback',
'items_wrap' => '%3$s',
'walker' => new Prefix_Nav_Walker(),
));
?>
The pattern is: <location>-<lang> (e.g., primary-en, primary-es, mobile-en, mobile-es).
When defining fields in inc/scf-fields.php for a bilingual site:
Use Tab fields to organize languages in the admin UI.
// English Tab
array(
'key' => 'field_hero_tab_en',
'label' => 'English',
'type' => 'tab',
'placement' => 'top',
),
array(
'key' => 'field_hero_title',
'label' => 'Hero Title',
'name' => 'hero_title',
'type' => 'text',
'required' => 1,
),
array(
'key' => 'field_hero_description',
'label' => 'Hero Description',
'name' => 'hero_description',
'type' => 'textarea',
'required' => 1,
),
// Spanish Tab
array(
'key' => 'field_hero_tab_es',
'label' => 'Espanol',
'type' => 'tab',
'placement' => 'top',
),
array(
'key' => 'field_hero_title_es',
'label' => 'Hero Title (ES)',
'name' => 'hero_title_es',
'type' => 'text',
'instructions' => 'Leave empty to use English version.',
'required' => 0,
),
array(
'key' => 'field_hero_description_es',
'label' => 'Hero Description (ES)',
'name' => 'hero_description_es',
'type' => 'textarea',
'instructions' => 'Leave empty to use English version.',
'required' => 0,
),
Inside repeaters, add suffixed subfields for each translatable text subfield.
array(
'key' => 'field_services',
'label' => 'Services',
'name' => 'services',
'type' => 'repeater',
'sub_fields' => array(
array(
'key' => 'field_service_icon',
'label' => 'Icon',
'name' => 'icon',
'type' => 'image',
),
array(
'key' => 'field_service_title',
'label' => 'Title (EN)',
'name' => 'title',
'type' => 'text',
),
array(
'key' => 'field_service_title_es',
'label' => 'Title (ES)',
'name' => 'title_es',
'type' => 'text',
'instructions' => 'Leave empty to use English version.',
),
array(
'key' => 'field_service_description',
'label' => 'Description (EN)',
'name' => 'description',
'type' => 'textarea',
),
array(
'key' => 'field_service_description_es',
'label' => 'Description (ES)',
'name' => 'description_es',
'type' => 'textarea',
'instructions' => 'Leave empty to use English version.',
),
array(
'key' => 'field_service_link',
'label' => 'Link',
'name' => 'link',
'type' => 'url',
// No _es version — URLs are typically language-neutral
),
),
),
Templates must NEVER call get_field() directly. Always use the translation-aware wrapper.
// WRONG — bypasses translation system
$title = get_field('hero_title');
// CORRECT — auto-translates based on current language
$title = prefix_get_field('hero_title');
This rule applies everywhere:
prefix_get_field() instead of get_field()prefix_get_sub_field() instead of get_sub_field()prefix_get_repeater() instead of raw get_field() on repeatersprefix__() / prefix_e() instead of hardcoded stringsThe only place get_field() is called directly is inside the helper functions themselves.
In header.php, set the document language dynamically.
<!DOCTYPE html>
<html <?php language_attributes(); ?> lang="<?php echo esc_attr(prefix_get_current_lang()); ?>">
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
The i18n system lives in a single file included early in functions.php.
// functions.php — i18n must load before SCF fields
require get_template_directory() . '/inc/i18n.php';
require get_template_directory() . '/inc/scf-fields.php';
The inc/i18n.php file contains:
prefix_get_current_lang()prefix_get_field()prefix_get_repeater()prefix_get_sub_field()prefix__() and prefix_e()prefix_get_lang_url()prefix_is_spanish() / prefix_is_lang()prefix_get_translations() (the static strings array)prefix_get_js_translations()PREFIX_SUPPORTED_LANGS and PREFIX_DEFAULT_LANG constants definedprefix_get_field() used in ALL templates (never raw get_field())prefix_get_repeater() used for repeater fields with translatable subfields specifiedprefix_get_sub_field() used inside have_rows() loopsprefix__() / prefix_e() used for all static UI strings_<lang> suffix and "Leave empty to use English version" instruction<location>-<lang>remove_query_arg / add_query_arglang attribute set dynamically