WordPress Çoklu Dil Desteği: functions.php ile Nasıl Yapılır?

Çok dilli bir WordPress sitesi kurmak, kulağa karmaşık gelir ama aslında functions.php dosyasına birkaç akıllıca fonksiyon eklemekle işin büyük kısmını halledebilirsiniz. Eklenti bağımlılığını minimumda tutmak, sitenizi hem daha hızlı hem de daha kontrol edilebilir kılar. Bu yazıda, WordPress’te çoklu dil desteğini functions.php üzerinden nasıl yöneteceğinizi, gerçek dünya senaryolarıyla birlikte ele alacağım.

Neden functions.php ile Çoklu Dil?

WPML veya Polylang gibi eklentiler güçlüdür ama beraberinde performans yükü getirir. Küçük ve orta ölçekli projelerde, özellikle sadece iki dil destekleyecekseniz, bu eklentilerin getirdiği veritabanı sorgu artışı ve sayfa yüklenme süresi ciddi bir sorun olabilir. functions.php ile yapılan özelleştirmeler size tam kontrol verir ve sitenizi gereksiz yükten kurtarır.

Şunu da belirteyim: Bu yaklaşım her senaryo için uygun değil. 5’ten fazla dil, karmaşık içerik çevirisi veya WooCommerce entegrasyonu gerektiren projelerde profesyonel eklentilere ihtiyaç duyarsınız. Ama temel çok dilli yapıyı anlamak, eklenti kullansanız bile işinize yarayacak.

Temel: WordPress Dil Altyapısını Anlamak

WordPress, kendi içinde .pot, .po ve .mo dosyaları üzerinden çeviri sistemi sunar. Tema ve eklentileriniz __(), _e(), esc_html__() gibi fonksiyonları kullanıyorsa, bu dosyaları hazırlayarak arayüz çevirisi yapabilirsiniz. functions.php burada devreye girerek hangi dil dosyasının yükleneceğini, URL yapısının nasıl şekilleneceğini ve kullanıcıya hangi içeriğin gösterileceğini belirler.

1. Tema Dil Desteğini Aktifleştirme

İlk adım, temanızın çeviri dosyalarını tanımasını sağlamak. Bu olmadan hiçbir şey çalışmaz.

// functions.php içine ekleyin
function mytheme_load_textdomain() {
    load_theme_textdomain( 'mytheme', get_template_directory() . '/languages' );
}
add_action( 'after_setup_theme', 'mytheme_load_textdomain' );

Bu kod, temanızın /languages klasöründeki .mo dosyalarını otomatik olarak yükler. Dosya adları tr_TR.mo, en_US.mo, de_DE.mo gibi locale kodlarıyla eşleşmelidir.

Şimdi tema içindeki statik metinlerinizi çevrilebilir hale getirin:

// Tema içinde kullanım örnekleri
echo __( 'Devam Et', 'mytheme' );
_e( 'Daha Fazla Oku', 'mytheme' );
$buton_metni = esc_html__( 'Satın Al', 'mytheme' );

// Değişken içeren metinler için
$mesaj = sprintf(
    __( 'Merhaba %s, hoş geldiniz!', 'mytheme' ),
    $kullanici_adi
);

2. URL Bazlı Dil Algılama ve Yönlendirme

Gerçek dünya senaryosunda en çok kullanılan yaklaşım URL üzerinden dil tespiti. Yani /en/about, /de/about, /tr/hakkimizda gibi yapılar. Bunu functions.php ile şöyle yönetebilirsiniz:

function mytheme_detect_language() {
    $current_url = $_SERVER['REQUEST_URI'];
    $lang        = 'tr'; // varsayılan dil

    if ( strpos( $current_url, '/en/' ) === 0 || $current_url === '/en' ) {
        $lang = 'en';
    } elseif ( strpos( $current_url, '/de/' ) === 0 || $current_url === '/de' ) {
        $lang = 'de';
    } elseif ( strpos( $current_url, '/fr/' ) === 0 || $current_url === '/fr' ) {
        $lang = 'fr';
    }

    // Dili session veya cookie ile sakla
    if ( ! session_id() ) {
        session_start();
    }
    $_SESSION['site_language'] = $lang;

    return $lang;
}

function mytheme_get_current_lang() {
    return mytheme_detect_language();
}

Bu fonksiyonu template dosyalarınızda mytheme_get_current_lang() şeklinde çağırarak aktif dili alabilirsiniz. URL başındaki dil koduna bakarak içeriği dinamik şekilde çekebilirsiniz.

3. Dil Switcher Menüsü Oluşturma

Ziyaretçilerin dil değiştirebilmesi için bir switcher gerekli. Bunu sıfırdan functions.php içinde oluşturalım:

function mytheme_language_switcher( $echo = true ) {
    $current_lang = mytheme_get_current_lang();
    $current_url  = home_url( $_SERVER['REQUEST_URI'] );

    $languages = array(
        'tr' => array(
            'label'  => 'Türkçe',
            'flag'   => '🇹🇷',
            'locale' => 'tr_TR',
            'slug'   => '',
        ),
        'en' => array(
            'label'  => 'English',
            'flag'   => '🇬🇧',
            'locale' => 'en_US',
            'slug'   => 'en',
        ),
        'de' => array(
            'label'  => 'Deutsch',
            'flag'   => '🇩🇪',
            'locale' => 'de_DE',
            'slug'   => 'de',
        ),
    );

    $output = '<ul class="lang-switcher">';

    foreach ( $languages as $code => $lang ) {
        $active_class = ( $code === $current_lang ) ? ' class="active"' : '';
        $lang_url     = home_url( '/' . ltrim( $lang['slug'] . '/', '/' ) );

        $output .= sprintf(
            '<li%s><a href="%s">%s %s</a></li>',
            $active_class,
            esc_url( $lang_url ),
            $lang['flag'],
            esc_html( $lang['label'] )
        );
    }

    $output .= '</ul>';

    if ( $echo ) {
        echo $output;
    } else {
        return $output;
    }
}

// Shortcode olarak da kullanabilmek için
add_shortcode( 'lang_switcher', function() {
    return mytheme_language_switcher( false );
} );

Template dosyalarınızda mytheme_language_switcher() veya içeriklerde [lang_switcher] shortcode’uyla kullanabilirsiniz.

4. Özel Post Type ile Çok Dilli İçerik Yönetimi

Bu, işin en kritik kısmı. İçeriklerinizi çevirmek için yaygın yaklaşım, her yazının bir “çeviri grubu” oluşturmasıdır. Bunu custom meta alanlarıyla yönetebilirsiniz:

// Çeviri ilişkisi kuran meta kutusu ekle
function mytheme_translation_meta_box() {
    $post_types = array( 'post', 'page', 'product' );

    foreach ( $post_types as $post_type ) {
        add_meta_box(
            'mytheme_translations',
            __( 'Çeviri Bağlantıları', 'mytheme' ),
            'mytheme_translation_meta_box_html',
            $post_type,
            'side',
            'default'
        );
    }
}
add_action( 'add_meta_boxes', 'mytheme_translation_meta_box' );

function mytheme_translation_meta_box_html( $post ) {
    $tr_id = get_post_meta( $post->ID, '_translation_tr', true );
    $en_id = get_post_meta( $post->ID, '_translation_en', true );
    $de_id = get_post_meta( $post->ID, '_translation_de', true );

    wp_nonce_field( 'mytheme_translation_nonce', 'translation_nonce' );
    ?>
    <p>
        <label>TR Post ID:</label>
        <input type="number" name="translation_tr"
               value="<?php echo esc_attr( $tr_id ); ?>" style="width:100%" />
    </p>
    <p>
        <label>EN Post ID:</label>
        <input type="number" name="translation_en"
               value="<?php echo esc_attr( $en_id ); ?>" style="width:100%" />
    </p>
    <p>
        <label>DE Post ID:</label>
        <input type="number" name="translation_de"
               value="<?php echo esc_attr( $de_id ); ?>" style="width:100%" />
    </p>
    <?php
}

function mytheme_save_translation_meta( $post_id ) {
    if ( ! isset( $_POST['translation_nonce'] ) ) {
        return;
    }
    if ( ! wp_verify_nonce( $_POST['translation_nonce'], 'mytheme_translation_nonce' ) ) {
        return;
    }
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }

    $fields = array( 'translation_tr', 'translation_en', 'translation_de' );

    foreach ( $fields as $field ) {
        if ( isset( $_POST[ $field ] ) ) {
            update_post_meta(
                $post_id,
                '_' . $field,
                absint( $_POST[ $field ] )
            );
        }
    }
}
add_action( 'save_post', 'mytheme_save_translation_meta' );

Bu yapıyla her yazıya, hangi dilin karşılığının hangi post olduğunu belirtebilirsiniz. Editörleriniz admin panelden kolayca bağlantı kurabilir.

5. Dile Göre İçerik Çekme Fonksiyonu

Yazı ID’lerini birbirine bağladıktan sonra, aktif dile göre doğru içeriği getiren bir yardımcı fonksiyon yazalım:

function mytheme_get_translated_post( $post_id = null, $lang = null ) {
    if ( ! $post_id ) {
        $post_id = get_the_ID();
    }
    if ( ! $lang ) {
        $lang = mytheme_get_current_lang();
    }

    // Mevcut yazı zaten doğru dildeyse döndür
    $current_post_lang = get_post_meta( $post_id, '_post_language', true );
    if ( $current_post_lang === $lang ) {
        return get_post( $post_id );
    }

    // Çeviri ID'sini al
    $translated_id = get_post_meta( $post_id, '_translation_' . $lang, true );

    if ( $translated_id && get_post_status( $translated_id ) === 'publish' ) {
        return get_post( $translated_id );
    }

    // Çeviri yoksa varsayılan dönüş
    return get_post( $post_id );
}

// Kullanım örneği template içinde:
// $translated = mytheme_get_translated_post();
// echo $translated->post_title;
// echo apply_filters( 'the_content', $translated->post_content );

6. Dile Göre Dinamik Menü Yükleme

Farklı diller için farklı menü göstermek isteyebilirsiniz. Türkçe navigasyon ayrı, İngilizce ayrı. Bunu functions.php üzerinden şöyle halledersiniz:

function mytheme_register_multilang_menus() {
    register_nav_menus( array(
        'main-menu-tr' => __( 'Ana Menü (TR)', 'mytheme' ),
        'main-menu-en' => __( 'Ana Menü (EN)', 'mytheme' ),
        'main-menu-de' => __( 'Ana Menü (DE)', 'mytheme' ),
        'footer-menu-tr' => __( 'Footer Menü (TR)', 'mytheme' ),
        'footer-menu-en' => __( 'Footer Menü (EN)', 'mytheme' ),
    ) );
}
add_action( 'after_setup_theme', 'mytheme_register_multilang_menus' );

function mytheme_get_lang_menu( $position = 'main', $echo = true ) {
    $lang     = mytheme_get_current_lang();
    $location = $position . '-menu-' . $lang;

    // Dil menüsü yoksa varsayılana düş
    $locations = get_nav_menu_locations();
    if ( ! isset( $locations[ $location ] ) ) {
        $location = $position . '-menu-tr';
    }

    $args = array(
        'theme_location' => $location,
        'menu_class'     => 'nav-menu lang-' . $lang,
        'container'      => 'nav',
        'echo'           => $echo,
        'fallback_cb'    => false,
    );

    return wp_nav_menu( $args );
}

Template dosyanızda mytheme_get_lang_menu( 'main' ) ile aktif dile ait menüyü çağırabilirsiniz. Eğer Almanca menü tanımlanmamışsa otomatik Türkçe menüye düşer.

7. Hreflang Etiketleri ve SEO Optimizasyonu

Çok dilli sitelerde SEO açısından en kritik nokta hreflang etiketleri. Google’a hangi sayfanın hangi dilin karşılığı olduğunu söylemeniz gerekiyor. Bu olmadan arama motoru her iki sayfayı duplicate content olarak işaretleyebilir.

function mytheme_add_hreflang_tags() {
    $post_id   = get_the_ID();
    $languages = array( 'tr', 'en', 'de' );
    $locale_map = array(
        'tr' => 'tr-TR',
        'en' => 'en-US',
        'de' => 'de-DE',
    );

    if ( ! $post_id ) {
        return;
    }

    echo "n<!-- Hreflang Tags -->n";

    foreach ( $languages as $lang ) {
        $translated_id = get_post_meta( $post_id, '_translation_' . $lang, true );

        if ( $translated_id && get_post_status( $translated_id ) === 'publish' ) {
            $url    = get_permalink( $translated_id );
            $locale = $locale_map[ $lang ];
            printf(
                '<link rel="alternate" hreflang="%s" href="%s" />' . "n",
                esc_attr( $locale ),
                esc_url( $url )
            );
        }
    }

    // x-default için ana dili ekle
    $default_id = get_post_meta( $post_id, '_translation_tr', true );
    if ( $default_id ) {
        printf(
            '<link rel="alternate" hreflang="x-default" href="%s" />' . "n",
            esc_url( get_permalink( $default_id ) )
        );
    }

    echo "<!-- /Hreflang Tags -->nn";
}
add_action( 'wp_head', 'mytheme_add_hreflang_tags' );

Bu fonksiyon, wp_head hook’una bağlanarak tüm sayfaların bölümüne otomatik hreflang etiketleri ekler. Google Search Console’da “Alternatif Sayfalar” raporunda bunların doğru göründüğünü kontrol etmeyi unutmayın.

Gerçek Dünya: WooCommerce Entegrasyonu

WooCommerce kullananlar için ürün açıklamalarını da çevirmek gerekiyor. Meta kutularımızı ürünlere de uyguladık ama kısa açıklamalar ve fiyat formatları için ek düzenleme lazım:

function mytheme_woo_multilang_setup() {
    $lang = mytheme_get_current_lang();

    // Para birimi sembolü dile göre
    if ( $lang === 'en' ) {
        add_filter( 'woocommerce_currency', function() {
            return 'USD';
        } );
        add_filter( 'woocommerce_currency_symbol', function( $symbol, $currency ) {
            return '$';
        }, 10, 2 );
    } elseif ( $lang === 'de' ) {
        add_filter( 'woocommerce_currency', function() {
            return 'EUR';
        } );
        add_filter( 'woocommerce_currency_symbol', function( $symbol, $currency ) {
            return '€';
        }, 10, 2 );
    }

    // WooCommerce e-posta dilini ayarla
    add_filter( 'woocommerce_email_subject_new_order', function( $subject ) use ( $lang ) {
        if ( $lang === 'en' ) {
            return 'New Order #{order_number} - {order_date}';
        }
        return 'Yeni Sipariş #{order_number} - {order_date}';
    } );
}
add_action( 'init', 'mytheme_woo_multilang_setup' );

Dikkat: Çok para birimli WooCommerce kurulumları karmaşıklaşabilir. Bu yaklaşımı yalnızca basit senaryolarda kullanın. Kurumsal e-ticaret projelerinde WPML + WooCommerce Multilingual eklenti kombinasyonu daha sağlıklıdır.

Performans: Dil Verilerini Cache’leme

Sürekli veritabanına gitmemek için basit bir obje cache mekanizması ekleyelim:

function mytheme_get_translation_cached( $post_id, $lang ) {
    $cache_key = 'mytheme_trans_' . $post_id . '_' . $lang;
    $cached    = wp_cache_get( $cache_key, 'mytheme_translations' );

    if ( false !== $cached ) {
        return $cached;
    }

    $translated_id = get_post_meta( $post_id, '_translation_' . $lang, true );
    $result        = $translated_id ? get_post( $translated_id ) : false;

    // 1 saat cache'le
    wp_cache_set( $cache_key, $result, 'mytheme_translations', 3600 );

    return $result;
}

// Post güncellenince cache temizle
function mytheme_flush_translation_cache( $post_id ) {
    $languages = array( 'tr', 'en', 'de', 'fr' );
    foreach ( $languages as $lang ) {
        $cache_key = 'mytheme_trans_' . $post_id . '_' . $lang;
        wp_cache_delete( $cache_key, 'mytheme_translations' );
    }
}
add_action( 'save_post', 'mytheme_flush_translation_cache' );
add_action( 'deleted_post', 'mytheme_flush_translation_cache' );

Redis veya Memcached kullanıyorsanız wp_cache_* fonksiyonları otomatik olarak bu sistemlere yazar. Persistent object cache olmayan bir sunucuda da çalışır, sadece performans kazanımı olmaz.

Dikkat Edilmesi Gereken Noktalar

  • Canonical URL: Her dil sayfasının kendi canonical etiketi doğru kurulmuş olmalı, aksi halde SEO sorunları kaçınılmaz.
  • Admin dili: get_user_locale() ile admin kullanıcısının kendi dilini arayüzde görmesini sağlayabilirsiniz, bu site dilinden bağımsızdır.
  • Yorum sistemi: Yorumların hangi dil sayfasına ait olduğunu takip etmek için comment meta kullanın.
  • Sitemap: Yoast SEO veya Rank Math kullanıyorsanız, her dil için ayrı sitemap ürettiğini kontrol edin.
  • Cookie ve GDPR: Dil seçimi için kullandığınız cookie’ler GDPR kapsamında değerlendirilebilir, bunu privacy policy’nize yansıtın.
  • Taşıma ve yedek: İçerikler arasındaki meta bağlantıları içeren bir migrasyon scripti yazın, sunucu taşımalarında post ID’leri değişebilir.

Test Süreci

Çok dilli yapıyı canlıya almadan önce şu kontrolleri mutlaka yapın:

  • Dil switcher doğru URL’ye yönlendiriyor mu?
  • Eksik çeviri olduğunda fallback dili devreye giriyor mu?
  • Google Search Console’da hreflang hataları var mı?
  • WooCommerce sipariş e-postaları doğru dilde gidiyor mu?
  • Sayfa hızını Lighthouse ile ölçün, eklenti bazlı çözümle karşılaştırın.

Sonuç

functions.php ile çok dilli WordPress kurulumu, doğru yapılandırıldığında hem performanslı hem de esnek bir çözüm sunar. Bu yazıda ele aldığımız yöntemleri adım adım uygulayarak iki veya üç dilli bir siteyi hiçbir eklenti olmadan yönetebilirsiniz.

Ancak şunu unutmayın: Bu yaklaşım bir temel sağlar, tüm ihtiyaçları karşılamaz. İçerik ekibi büyüyünce, çevirmen erişim talepleri artınca veya 5+ dil gerektiğinde Polylang veya WPML’e geçmek daha mantıklı olacak. functions.php yolunu bilmek, o eklentilerin perde arkasında ne yaptığını anlamanızı da sağlar. Sistemi anlayan sysadmin, her zaman daha iyi karar verir.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir