Çoklu Dil (i18n) Sistemi

Üç dil, üç ayrı çoğul kuralı ve sağdan sola yazılan bir arayüz. intl ile ICU çoğul kuralları, yer tutucular, yerelleştirilmiş sayı/tarih/para ve ayrı CSS gerektirmeyen RTL desteği — oturum girişli bir panelin içinde.

PHP 8 PDO MySQL Bootstrap 5 Ajax Oturum Girişi
Seviye
Orta
Dosya
81
Kod satırı
~19.794
Proje boyutu
2.3 MB
Veritabanı
cy_i18n
Lisans
MIT
İnceleme
30
Beğeni
0
Yayın

Ekran Görüntüleri 7 görsel

Bu Örnek Ne Yapıyor?

  • Çoğul kuralını if değil, intl belirler — Arapça'da altı kategori (ikili dahil)
  • Cümle parçalanmaz; yer tutucu kullanılır, çevirmen sözcük sırasını değiştirebilir
  • Dil çözümleme sırası: ?lang= → hesap → çerez → tarayıcı → varsayılan
  • Dil tercihi hem çerezde hem hesapta saklanır; cihaz değişince kaybolmaz
  • Dil değiştirince ana sayfaya atmaz; bulunduğunuz sayfada kalırsınız
  • RTL için ayrı CSS dosyası yok — mantıksal özelliklerle düzen aynalanır
  • intl eklentisi yoksa uygulama çökmez, sade yedeğe düşer
  • Üç dilde de tam 73 anahtar; eksik anahtarda anahtarın kendisi basılır

Nerede İşe Yarar?

  • Projesine ikinci veya üçüncü dili ekleyecek ekipler
  • Arapça, Farsça veya İbranice desteği verecek uygulamalar
  • Çoğul eki ve sayı/tarih biçimlerini doğru kurmak isteyenler
  • Kütüphaneye bağlanmadan PHP'nin kendi intl eklentisiyle çalışmak isteyenler

Gereksinimler

  • PHP 8.0+ · MySQL 5.7+ / MariaDB 10.3+ · pdo_mysql · mbstring · intl (önerilir) · Apache mod_rewrite

Nasıl Kurulur?

  1. Depoyu klonlayın veya ZIP olarak indirip web kökünüze açın
  2. database.sql dosyasını içe aktarın (veritabanını kendisi oluşturur)
  3. .env.example dosyasını .env adıyla kopyalayıp veritabanı bilgilerinizi yazın
  4. php.ini içinde extension=intl satırını açın (önerilir, zorunlu değil)
  5. Tarayıcıdan açıp [email protected] / Admin1234 ile giriş yapın

Veritabanı şeması projedeki database.sql dosyasında.

Nasıl Çalışıyor?

Çılgın Yazılım

Çoklu Dil (i18n) Sistemi

PHP 8 · PDO · MySQL · Oturum Girişli Panel · Çoğul Kuralları · RTL · Çılgın Yazılım Tasarım Kalıbı

Üç dil, üç ayrı çoğul kuralı ve sağdan sola yazılan bir arayüz — kütüphanesiz.

PHP
MySQL
Bootstrap
Composer
License

🇹🇷 Türkçe · 🇬🇧 English

▶ Canlı Demo · Kaynak Kütüphanesi · cilginyazilim.com


Canlı Demo

Kurulum yok, kayıt yok, indirme yok — tarayıcınızdan 3 saniyede deneyin.

Canlı Demoyu Aç
Kaynak Kodu İncele
ZIP İndir

Çoklu dil sistemi canlı demo önizlemesi

▲ Görsele tıklayarak demoyu açabilirsiniz

Demo hesapları

RolE-postaParola
Yönetici[email protected]Admin1234
Kullanıcı[email protected]Demo1234

Giriş ekranındaki hazır düğmelerle alanları tek tıkla doldurabilirsiniz.

Demoda 60 saniyede neleri deneyebilirsiniz?

#Şunu deneyinPerde arkasında ne oluyor?
1Üst çubuktaki dil seçicisinden EN seçinSayfa yenilenir ama bulunduğunuz sayfada kalırsınız. Dil seçici ana sayfaya atmaz; bulunduğunuz adrese ?lang= ekler
2Şimdi AR seçinBütün düzen aynalanır: kenar çubuğu sağa geçer, metin sağa yaslanır, tablo sütunları ters sıraya döner. Tek bir "mobil sürüm" dosyası yok; CSS mantıksal özelliklerle yazıldığı için dir="rtl" yetiyor
3Dil Örnekleri sayfasında "Çoğul kuralları" tablosuna bakın0, 1, 2, 5, 11, 100 sayıları için üç dilin farklı karşılıkları yan yana durur. Türkçe tek biçim, İngilizce iki, Arapça altı biçim kullanır
4Aynı tabloda Arapça sütununda 2 satırına bakınسجلان yazar — bu ne tekil ne çoğuldur, Arapça'nın ikili (dual) biçimidir. if ($n == 1) yazan bir kod bunu asla üretemez
5"Yer tutucular" bölümündeki cümleyi Arapça'da okuyunHoş geldiniz, Sistem! cümlesi Arapça'da sözcük sırası değişerek kurulur. Cümleyi 'Hoş geldin, ' . $ad diye parçalasaydınız bu mümkün olmazdı
6"Sayı, tarih ve para" tablosuna bakınAynı sayı üç dilde üç farklı biçimde yazılır: binlik ayracı, ondalık ayracı ve para biriminin cümledeki yeri değişir. Bunu number_format() değil, intl yapar
7Çıkış yapıp AR seçin, sonra giriş yapınDil seçiminiz giriş ekranından panele taşınır; hesabınıza yazılır. Çerezi silseniz bile tercihiniz kaybolmaz
8Sağ üstteki ay/güneş simgesine basınKoyu tema açılır ve hesabınıza kaydedilir. Tablo hücrelerinin kontrastı koyu temada da ölçülüdür (14,5:1)
9Kullanıcılar sayfasında arama kutusuna ş yazınArama sunucuda çalışır; LIKE jokerleri kaçışlanır ve sıralama sütunu beyaz listeden doğrulanır
10Telefonunuzdan açınKenar çubuğu alt navigasyona dönüşür; sayfa gövdesinde yatay kaydırma yoktur
İpucu: Demoyu açıkken adres çubuğuna ?lang=ar ekleyin. Dilin nasıl seçildiğini görmek için Dil nasıl seçilir? bölümüne bakın — sıra ?lang= → hesap → çerez → tarayıcı → varsayılan şeklindedir.

Demo alanı hakkında bilinmesi gerekenler

KonuDurum
Verilerdatabase.sql içindeki 51 örnek kullanıcı. Gerçek kişi verisi yoktur; adresler @ornek.com uzantılıdır.
SıfırlamaDemo veritabanı düzenli aralıklarla başlangıç hâline döner; yaptığınız değişiklikler kalıcı değildir.
Kimlik doğrulamaVardır. Oturum, "beni hatırla" jetonu, hız sınırı ve CSRF korumasıyla birlikte gelir.
APP_DEBUGCanlıda kendiliğinden false — sunucu adından türetilir, yerelde true kalır.
intl eklentisiDemo sunucusunda açıktır. Kapalı olsaydı uygulama çalışmaya devam eder, yalnızca yerel biçimler sadeleşirdi.
BağımlılıkSıfır. Composer yok, npm yok, CDN yok. Demo internetsiz bir sunucuda da aynı çalışır.
Demo geçici olarak kapalıysa endişelenmeyin: depoyu klonlayıp database.sql'i içe aktarmanız aynı ekranı kendi bilgisayarınızda 2 dakikada ayağa kaldırır → Kurulum

Bu Proje Nedir?

"Siteyi çok dilli yapalım" cümlesi, çoğu projede bir dizi dosyası açıp $lang['kaydet'] = 'Kaydet'; yazmakla başlar ve orada biter. Sonra ilk gerçek cümle gelir — "5 kayıt bulundu" — ve sistem çöker. Çünkü:

  • Türkçe'de sayıdan sonra çoğul eki gelmez: "5 kayıt", "5 kayıtlar" değil.
  • İngilizce'de iki biçim vardır: "1 record" / "5 records".
  • Arapça'da altı biçim vardır ve bunlardan biri ikilidir: 2 için ne tekil ne çoğul, bambaşka bir sözcük kullanılır.

if ($n == 1) yazan kod İngilizce için doğru, Türkçe için gereksiz, Arapça için tamamen yanlıştır. Kural dile aittir; koda değil.

Bu proje o kuralı dilin kendisine bırakan bir i18n katmanı kuruyor: çeviriler PHP dizisi olarak lang// altında durur, çoğul biçimi intl eklentisinin MessageFormatter seçer, sayı/tarih/para biçimleri NumberFormatter ve IntlDateFormatter ile yerelleştirilir ve arayüz Arapça'ya geçtiğinde tek bir ek CSS dosyası olmadan sağdan sola döner.

Dahası, bunların hepsi oturum girişli, gerçek bir panelin içinde çalışır: kullanıcının dil tercihi hesabına yazılır, koyu tema tercihiyle birlikte cihazdan cihaza taşınır.

Kimler için uygun?

  • Projesine ikinci (veya üçüncü) dili eklemek üzere olanlar
  • "Çoğul eki nasıl yapılır?" sorusunun cevabının if olmadığını öğrenmek isteyenler
  • Arapça, Farsça veya İbranice desteği verecek olanlar (RTL)
  • Kütüphaneye bağlanmadan, PHP'nin kendi intl eklentisiyle iş görmek isteyenler
  • Bootstrap 5 üzerine kurulu, tekrar kullanılabilir bir panel kalıbı arayanlar
Klonla, database.sql'i içe aktar, çalıştır. Başka hiçbir kurulum adımı yok. Composer yok, npm yok, internet bağlantısı bile gerekmiyor — tüm kütüphaneler proje içinde.

Bu proje, Çılgın Yazılım Kütüphanesi altında yayınlanan açıklamalı, üretime hazır örneklerden biridir.


İçindekiler


Ekran Görüntüleri

Dil örnekleri

Projenin konusu bu sayfada toplanır. Çoğul kuralları tablosunda 0, 1, 2, 5, 11, 100 sayılarının üç dildeki karşılıkları yan yana durur: Türkçe tek biçim, İngilizce iki, Arapça altı biçim kullanır. Altında yer tutuculu cümleler, sayı/tarih/para biçimleri ve göreli zaman aynı ekranda görülür — hepsini intl üretir, hiçbirini if üretmez.

Dil örnekleri: çoğul kuralları, yer tutucular ve yerelleştirilmiş sayı, tarih, para biçimleri

Arapça (RTL)

Dil seçicisinden AR seçildiğinde bütün düzen aynalanır: kenar çubuğu sağa geçer, metin sağa yaslanır, tablo sütunları ters sıraya döner. Ayrı bir "RTL sürümü" CSS dosyası yoktur; arayüz mantıksal özelliklerle (margin-inline-start, padding-inline) yazıldığı için dir="rtl" tek başına yetiyor.

Arapça sağdan sola görünüm: kenar çubuğu sağda, metin sağa yaslı, sütunlar ters sırada

Kullanıcılar

Sunucu taraflı sayfalama ve filtreleme. Arama kutusu ile açılır listeler "Uygula"ya basmadan çalışır: listeler anında, arama kutusu 450 ms yazma beklemesiyle. Filtre ve sayfa numarası adres çubuğunda taşındığı için bağlantı paylaşılabilir, geri tuşu ve yenileme aynı sonucu verir.

Kullanıcı listesi: canlı çalışan arama ve durum filtresi, sunucu taraflı sayfalama

Kontrol paneli

Sayaç şeridi ve dil katmanının özeti. Kullanıcının dil tercihi tarayıcıda değil hesabında durur; koyu tema tercihiyle birlikte cihazdan cihaza taşınır.

Kontrol paneli: sayaç şeridi ve dil katmanının özeti

Giriş ekranı

Demo hesapları tek tıkla doldurulur. Giriş denemeleri hız sınırına tabidir; art arda başarısız denemeden sonra hesap geçici olarak kilitlenir. Giriş ekranında seçilen dil, girişten sonra hesaba taşınır.

Giriş ekranı: demo hesapları tek tıkla doldurulur

Koyu tema

Tema tarayıcıda değil kullanıcı hesabında saklanır. Tablo hücrelerinin kontrastı koyu temada da ölçülüdür (14,5:1).

Koyu tema görünümü

Mobil görünüm

390px genişlikte kenar çubuğu alt navigasyona dönüşür. Sayfa gövdesinde yatay kaydırma yoktur; geniş tablolar yalnızca kendi kapsayıcılarında kayar.

390px genişlikte mobil görünüm

Kritik Kararlar

Bu bölüm, "neden böyle yapılmış?" sorusunun cevabıdır. Her madde bilerek verilmiş bir karardır.

1. Çoğul kuralını if değil, intl belirler

Yanlış olan:

$metin = $n == 1 ? '1 kayıt' : "$n kayıtlar";

Bu kod İngilizce'nin kuralını bütün dillere dayatır. Doğrusu, kuralı dile sormaktır:

echo n('common.records', $count);

Arka planda MessageFormatter çalışır ve çeviri dosyasındaki ICU kalıbını kullanır:

// lang/ar/common.php
'records' => '{n, plural, zero{لا توجد سجلات} one{سجل واحد} two{سجلان} few{# سجلات} many{# سجلاً} other{# سجل}}',

Arapça'nın altı kategorisi (zero, one, two, few, many, other) burada görünür. Türkçe dosyasında yalnızca other vardır — çünkü Türkçe'nin ihtiyacı odur. Kod hiçbir dilin kuralını bilmez.

2. Cümle parçalanmaz, yer tutucu kullanılır

Yanlış olan:

echo t('welcome_prefix') . ' ' . $user->name . '!';

Bu, sözcük sırasının bütün dillerde aynı olduğunu varsayar. Arapça'da değildir. Doğrusu:

echo t('common.auth.welcome', ['name' => $user->name]);

Çevirmen artık cümlenin tamamını görür ve kendi dilinin sırasına göre kurar. Yer tutucu nereye giderse gitsin çalışır.

3. Dil seçimi beş kaynaktan, belirli bir sırayla gelir

Translator::resolve() şu sırayı izler:

?lang=  →  hesap tercihi  →  çerez  →  tarayıcı (Accept-Language)  →  varsayılan

Sıra rastgele değildir: en dıştaki açık talep (adres), en içteki tahmintir (tarayıcı). Adresteki seçim her şeyi ezer, çünkü kullanıcı o an bilerek istemiştir.

Gelen değer her adımda Translator::supports() ile beyaz listeden geçer; Translator::LOCALES dizisinde tanımlı olmayan bir dil kodu asla kabul edilmez. Aksi hâlde ?lang=../../etc/passwd bir dosya yolu hâline gelirdi.

4. Dil tercihi hem çerezde hem hesapta durur

Yalnızca çerezde tutulsaydı, kullanıcı işten eve geçtiğinde veya çerezleri temizlediğinde tercihi kaybolurdu. Yalnızca hesapta tutulsaydı, giriş yapmamış ziyaretçi dil seçemezdi.

İkisi birden: ziyaretçi çerezle, üye hesabıyla taşınır. Giriş anında çerezdeki seçim hesaba yazılır.

5. Dil değiştirince bulunduğunuz sayfada kalırsınız

Çoğu örnekte dil seçici sizi ana sayfaya atar. Bu, 8. sayfadaki bir listeyi okurken dili değiştirdiğinizde okuduğunuz yeri kaybetmeniz demektir.

Burada seçici, bulunduğunuz adrese ?lang= ekler. Sorgu dizesindeki diğer parametreler (page, q, per) korunur.

6. RTL için ayrı CSS dosyası yok

left/right yerine mantıksal özellikler kullanılır:

/* Yanlış: Arapça'da ters tarafa gider */
.cy-card { padding-left: 1rem; border-left: 3px solid; }

/* Doğru: yön neyse ona uyar */
.cy-card { padding-inline-start: 1rem; border-inline-start: 3px solid; }

` yazıldığı anda düzen kendiliğinden aynalanır. Bakımı iki kat artıran bir rtl.css` dosyası yoktur.

7. intl yoksa uygulama çökmez

intl eklentisi PHP'de varsayılan olarak açık değildir. Kapalıysa MessageFormatter bulunamaz ve uygulama ölürdü.

Bunun yerine Translator her yerelleştirme çağrısında eklentinin varlığını denetler ve yoksa sade bir yedeğe düşer: çoğulda other biçimi, sayıda number_format(), tarihte date(). Site çalışmaya devam eder, yalnızca biçimler sadeleşir. Kontrol panelindeki "İNTL" satırı hangi modda olduğunuzu söyler.


Neler Var?

Çeviri katmanı

  • t() — metin çevirisi, yer tutuculu
  • te() — çevir ve HTML'e kaçışlayarak bas
  • n() — çoğul kuralına uyan çeviri
  • Nokta notasyonu (common.nav.users)
  • Eksik anahtarda anahtarın kendisi basılır (boş metin değil)
  • Çeviri dosyaları düz PHP dizisi — derleme yok
  • Üç dil: Türkçe, İngilizce, Arapça · 73 anahtar, üçünde de tam

Yerelleştirme

  • intl ile çoğul kuralları (ICU)
  • NumberFormatter ile sayı ve para
  • IntlDateFormatter ile tarih ve saat
  • intl yoksa otomatik sade yedek

Panel ve güvenlik

  • Oturum girişi, "beni hatırla" jetonu
  • Giriş denemesi hız sınırı (login_attempts)
  • CSRF koruması (hash_equals)
  • Sertleştirilmiş oturum: HttpOnly, SameSite, kimlik yenileme
  • İçerik Güvenlik Politikası (script-src 'self')
  • X-Frame-Options: DENY, nosniff, Referrer-Policy

Arayüz

  • Çılgın Yazılım tasarım kalıbı
  • Açık / koyu tema, hesaba kayıtlı
  • Sunucu tarafında sayfalama ve arama
  • Mobilde alt navigasyon, yatay kaydırma yok
  • Kullanıcılar sayfasında canlı filtre (JS kapalıysa da çalışır)
  • Sıfır bağımlılık: Composer yok, npm yok, CDN yok

Çeviri dört iştir

Çok dillilik tek bir iş değildir; birbirine benzeyen ama farklı kuralları olan dört iştir. Uygulama dördünü de ayrı ayrı ele alır.

1 · Metin çevirisi. En basit hâli: anahtar ver, çevrilmiş metni al.

echo t('common.save');        // Kaydet / Save / حفظ

2 · Yer tutucular. Cümlenin içine değişken girdiğinde.

echo t('common.auth.welcome', ['name' => $user->name]);

3 · Çoğul. Sayıya göre biçim değiştiğinde. Kuralı dil belirler:

echo n('common.records', $count);

4 · Biçimlendirme. Sayı, tarih ve para birimi. Aynı değer her dilde farklı yazılır:

echo local_number(1234567.89, 2);  // 1.234.567,89  /  1,234,567.89
echo local_date($tarih);           // 3 Eylül 2026  /  September 3, 2026
echo local_money(199.90, 'TRY');   // ₺199,90       /  ₺199.90

Çoğul kuralları

Bir tablo her şeyi anlatır. n('common.records', $count) çağrısının üç dildeki çıktısı:

$countTürkçeİngilizceArapça
0Kayıt yokNo recordsلا توجد سجلات
11 kayıt1 recordسجل واحد
22 kayıt2 recordsسجلان
55 kayıt5 records5 سجلات
1111 kayıt11 records11 سجلاً
100100 kayıt100 records100 سجل

Dikkat edilecek üç nokta:

  1. Türkçe sütunu hiç değişmiyor (0 hariç). Türkçe'de sayıdan sonra çoğul eki gelmez.
  2. Arapça'da 2 satırı diğerlerine benzemiyor: سجلان ikili biçimdir, tekil de çoğul da değildir.
  3. Arapça'da 5, 11 ve 100 üç farklı biçim kullanıyor (few, many, other).

Bunların hiçbiri PHP kodunda yazılı değildir. Üçü de çeviri dosyasındaki ICU kalıbından gelir.


Dil nasıl seçilir?

İstek gelir
   │
   ├── 1) ?lang=en var mı?          → beyaz listede mi? → evet ise KULLAN
   │        (kullanıcı az önce açıkça istedi)
   │
   ├── 2) Giriş yapmış mı?          → users.locale dolu mu? → KULLAN
   │        (hesabına kaydedilmiş tercih)
   │
   ├── 3) cy_locale çerezi var mı?  → beyaz listede mi? → KULLAN
   │        (giriş yapmamış ziyaretçinin önceki seçimi)
   │
   ├── 4) Accept-Language başlığı    → desteklenen bir dille eşleşiyor mu? → KULLAN
   │        (tarayıcının tahmini)
   │
   └── 5) Hiçbiri yoksa             → varsayılan dil (tr)

Her adımda gelen değer Translator::LOCALES dizisindeki anahtarlarla karşılaştırılır (Translator::supports()). Listede olmayan hiçbir değer kabul edilmez — dil kodu bir dosya yoluna dönüştüğü için bu kontrol güvenliğin parçasıdır.

Dil değiştirince sayfada kalırsınız

Dil seçici, o anki adresi alır ve yalnızca lang parametresini değiştirir:

// views/partials/langswitch.php
<a href="<?= e(lang_url($kod)) ?>">…</a>

lang_url() yardımcısı o anki adresi alır, sorgu dizesindeki diğer parametreleri korur ve yalnızca lang değerini değiştirir.

?page=8&q=ahmet ile geldiyseniz dili değiştirdikten sonra da 8. sayfada, aynı aramadasınızdır.


RTL desteği

Arapça, Farsça ve İbranice sağdan sola yazılır. Bu, "metni sağa yasla" demek değildir; bütün düzenin aynalanması demektir.

` etiketine dir` yazılır:

<html lang="<?= e($locale) ?>" dir="<?= e($direction) ?>">

Translator::direction() dil koduna bakar ve rtl veya ltr döner. Yön, dilin Translator::LOCALES içindeki kaydında durur:

'ar' => ['name' => 'Arapça', 'native' => 'العربية', 'dir' => 'rtl', 'flag' => 'AR'],

Gerisi CSS'in işidir.

Altın kural: mantıksal özellikler kullanın

Fiziksel (kaçının)Mantıksal (kullanın)
margin-leftmargin-inline-start
padding-rightpadding-inline-end
border-leftborder-inline-start
text-align: lefttext-align: start
left: 0inset-inline-start: 0

Mantıksal özellikler "sol/sağ" değil, "başlangıç/bitiş" der. Başlangıç, LTR'de soldur; RTL'de sağdır. Böylece tek bir kural iki yönde de doğru çalışır.

Kaçınılmaz istisnalar için (örneğin yön belirten bir ok simgesi) tek satırlık bir kural yeterlidir:

[dir="rtl"] .cy-arrow { transform: scaleX(-1); }

Yeni dil eklemek

Üç adım. Kod değişikliği yok.

1 · Klasörü oluşturun ve çeviriyi kopyalayın

mkdir -p lang/de
cp lang/en/common.php lang/de/common.php

2 · Değerleri çevirin. Anahtarlara dokunmayın; yalnızca sağ tarafı değiştirin.

Çoğul kalıplarında o dilin kategorilerini kullanın. Almanca'da İngilizce gibi iki kategori vardır:

'records' => '{n, plural, zero{Keine Einträge} one{1 Eintrag} other{# Einträge}}',

3 · Dili kayıt defterine ekleyin

Desteklenen diller Translator::LOCALES dizisinde durur. Her kayıt dilin
adını, kendi dilindeki adını, metin yönünü ve seçicide görünecek kısa
kodunu taşır:

// app/Core/Translator.php
public const LOCALES = [
    'tr' => ['name' => 'Türkçe',  'native' => 'Türkçe',   'dir' => 'ltr', 'flag' => 'TR'],
    'en' => ['name' => 'İngilizce','native' => 'English',  'dir' => 'ltr', 'flag' => 'EN'],
    'ar' => ['name' => 'Arapça',  'native' => 'العربية',  'dir' => 'rtl', 'flag' => 'AR'],
    'de' => ['name' => 'Almanca', 'native' => 'Deutsch',  'dir' => 'ltr', 'flag' => 'DE'],
];

Sağdan sola yazılan bir dil ekliyorsanız yalnızca 'dir' => 'rtl' yazmanız
yeterlidir; ayrı bir liste yoktur. Dil seçici yeni dili kendiliğinden gösterir
ve Translator::supports() beyaz listesi de bu diziden türer.

4 · users.locale sütununu genişletin

Bu adım atlanırsa yeni dil görünür ama hesaba kaydedilemez:

ALTER TABLE `users`
  MODIFY COLUMN `locale` ENUM('tr','en','ar','de') NOT NULL DEFAULT 'tr';

Sütunun neden ENUM olduğunu ve ödünleşimini
Veritabanı Şeması bölümünde anlattık.

Eksik anahtar bırakırsanız ne olur? Uygulama çökmez; ekranda anahtarın kendisi görünür (common.save). Bu bilerek seçilmiştir: boş bir metin sessizce kaybolur, anahtar ise "burada eksik çeviri var" diye bağırır.

Güvenlik: Neyi, Nasıl Kapattık?

AçıkTipik hatalı kodBu projede
Yol geçişi (dil kodu)require "lang/$_GET[lang]/common.php"Dil kodu SUPPORTED beyaz listesinden doğrulanır; listede yoksa varsayılana düşer
SQL enjeksiyonu"... WHERE email = '$email'"Tüm sorgular hazır ifade; ATTR_EMULATE_PREPARES = false
Sıralama sütunu enjeksiyonuORDER BY $_GET[sort]Sütun adı beyaz listeden; yön yalnızca ASC/DESC
XSSecho $user->nameSunucuda e(), çeviride te(); ayrıca CSP script-src 'self'
CSRFGizli alan yokHer POST'ta jeton; karşılaştırma hash_equals() ile (zamanlama saldırısına kapalı)
Parola sızıntısımd5($parola)password_hash() / password_verify(); algoritma eskiyince otomatik yeniden özetleme
Kullanıcı sayımı"Böyle bir e-posta yok"Hem yanlış e-postada hem yanlış parolada aynı mesaj ve aynı süre (sahte özet doğrulaması)
Kaba kuvvetSınırsız denemelogin_attempts tablosuyla e-posta + IP başına hız sınırı
Oturum çalmaSabit oturum kimliğiGirişte kimlik yenilenir; çerez HttpOnly + SameSite=Lax + HTTPS'te Secure
Hata sızıntısıCanlıda display_errors=OnAPP_DEBUG ortamdan türetilir; gerçek alan adında kendiliğinden kapanır
Tıklama hırsızlığıBaşlık yokX-Frame-Options: DENY
Bozuk UTF-8'de sessiz JSON kaybıjson_encode($v)JSON_INVALID_UTF8_SUBSTITUTE — bozuk bayt tüm yanıtı yutmaz

Kurulum

Gereksinimler

PHP8.0 veya üzeri
MySQL / MariaDB5.7+ / 10.3+
Web sunucusuApache (mod_rewrite) veya Nginx
PHP eklentileripdo_mysql, mbstring · önerilen: intl

intl zorunlu değildir; yoksa uygulama sade yedeğe düşer (bkz. Kritik Kararlar #7).

Adımlar

1 · Dosyaları yerleştirin

git clone https://github.com/CilginYazilim/multi-language-system.git
cd multi-language-system

2 · Veritabanını içe aktarın

mysql -u root -p < database.sql

Dosya veritabanını kendisi oluşturur (cy_i18n), tabloları kurar ve 51 örnek kullanıcı yükler.

3 · .env dosyasını oluşturun

cp .env.example .env        # Windows: copy .env.example .env

İçindeki veritabanı bilgilerini kendinize göre düzenleyin.

4 · Açın

http://localhost/multi-language-system/

Giriş: [email protected] / Admin1234

intl eklentisini açmak

php.ini içinde şu satırın başındaki ; işaretini kaldırın ve sunucuyu yeniden başlatın:

extension=intl

Açık olup olmadığını kontrol panelindeki İNTL satırından görebilirsiniz.


Yapılandırma

Bütün ayarlar config/config.php içinde tek bir dizide toplanır; hassas değerler .env dosyasından okunur.

APP_DEBUG=true          # silerseniz: yerelde açık, canlıda kapalı
APP_URL=                # boş bırakılırsa adres otomatik tespit edilir
APP_PRETTY_URLS=true    # false → /index.php?r=users

DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=cy_i18n
DB_USER=root
DB_PASS=
••••••••

> **`.env` dosyası `.gitignore` içindedir.** Parolanız asla depoya gitmez. `APP_DEBUG` satırını tümüyle silerseniz uygulama ortamdan karar verir: `localhost`, `*.test` ve `*.local` adreslerinde açık, gerçek bir alan adında kapalı.

**mod_rewrite yoksa:** `.env` içinde `APP_PRETTY_URLS=false` yapın. Uygulama hiçbir kod değişikliği olmadan `/index.php?r=users` biçimine döner.

---

## Kendi Projenize Eklemek

Çeviri katmanı üç dosyadan ibarettir ve panelden bağımsız çalışır.

**1 · Kopyalayın**

app/Core/Translator.php → çeviri motoru
app/Support/helpers.php → t() te() n() local_number() local_date() local_money()
lang/ → çeviri dosyaları

**2 · İsteğin başında dili belirleyin.** Bu, **görünümlerden önce** olmalıdır; yoksa ilk basılan metinler varsayılan dilde kalır.

```php
$locale = Translator::resolve($request);
Translator::setLocale($locale);

3 · `` etiketine yön yazın

<html lang="<?= e($locale) ?>" dir="<?= e(Translator::direction($locale)) ?>">

4 · Metinleri değiştirin

// önce
<h1>Kullanıcılar</h1>

// sonra
<h1><?= te('common.nav.users') ?></h1>

Translator sınıfının veritabanına bağımlılığı yoktur; yalnızca dosya okur. Kullanıcının tercihini hesabında saklamak isterseniz users tablosuna bir locale sütunu ekleyin.


Dosya Yapısı

multi-language-system/
│
├── index.php                  Ön denetleyici — TEK giriş noktası
├── database.sql               Şema + 51 örnek kullanıcı
├── .env.example               Ortam değişkeni şablonu
│
├── app/
│   ├── Core/
│   │   ├── Translator.php     ★ Çeviri motoru, çoğul, biçimlendirme, RTL
│   │   ├── Auth.php           Giriş, "beni hatırla", parola doğrulama
│   │   ├── Session.php        Sertleştirilmiş oturum
│   │   ├── Csrf.php           Jeton üretimi ve hash_equals doğrulaması
│   │   ├── RateLimiter.php    Giriş denemesi sınırı
│   │   ├── Router.php         Rota eşleme
│   │   ├── Database.php       PDO bağlantısı (EMULATE_PREPARES = false)
│   │   ├── Paginator.php      Sayfalama hesabı
│   │   ├── Env.php            .env okuyucu + isLocalHost()
│   │   └── ...
│   │
│   ├── Http/Controllers/      Auth · Dashboard · I18n · User · Api
│   ├── Models/User.php
│   ├── Repositories/          Veritabanı sorguları
│   └── Support/helpers.php    t() te() n() local_* lang_url() e() url()
│
├── lang/
│   ├── tr/common.php          73 anahtar
│   ├── en/common.php          73 anahtar
│   └── ar/common.php          73 anahtar (altı çoğul kategorisiyle)
│
├── views/
│   ├── layouts/               admin · auth · plain
│   ├── partials/              topbar · sidebar · bottomnav · langswitch · pagination
│   ├── auth/login.php
│   ├── dashboard/index.php
│   ├── i18n/index.php         ★ Dil örnekleri sayfası
│   ├── users/index.php
│   └── errors/                404 · 500
│
├── assets/
│   ├── css/  cilginyazilim.css (marka) · admin.css · feature.css · bootstrap.min.css
│   └── js/   app.js · lang.js · login.js · users.js · jquery · bootstrap
│
├── config/config.php          Tek merkez ayarlar
├── routes/web.php             Rota tanımları
└── docs/screenshots/          README görselleri

Nasıl Çalışıyor?

Tarayıcı
   │  GET /multi-language-system/i18n?lang=ar
   ▼
.htaccess ──► index.php  (ön denetleyici)
   │
   ├─ 1. Autoloader        sınıfları yükle
   ├─ 2. .env + config     veritabanı künyesi, hata modu
   ├─ 3. Hata yönetimi     yakalanmamış hata → düzgün 500 sayfası
   ├─ 4. Session::start()  sertleştirilmiş oturum
   ├─ 5. Güvenlik başlıkları  CSP · X-Frame-Options · nosniff
   │
   ├─ 6. DİL SEÇİMİ  ◄── ROTALARDAN ÖNCE OLMALI
   │      Translator::resolve()
   │        ?lang= → hesap → çerez → tarayıcı → varsayılan
   │      Translator::setLocale('ar')
   │        └─ lang/ar/common.php belleğe alınır
   │
   ├─ 7. View::share()     locale + direction bütün görünümlere
   │
   └─ 8. Router::dispatch()
            │
            ▼
         I18nController::index()
            │
            ▼
         views/i18n/index.php
            │  t('...')  n('...', 5)  fmt_money(...)
            ▼
         Translator
            │
            ├─ intl VAR   → MessageFormatter · NumberFormatter · IntlDateFormatter
            └─ intl YOK   → other biçimi · number_format() · date()
            │
            ▼
         layouts/admin.php
            <html lang="ar" dir="rtl">   ◄── düzen aynalanır

Veritabanı Şeması

Bu proje yeni bir tablo eklemez; var olan users tablosuna bir sütun ekler.

ALTER TABLE `users`
  ADD COLUMN `locale` ENUM('tr','en','ar') NOT NULL DEFAULT 'tr' AFTER `theme`;
KararNeden
Dil tercihi hesapta tutuluyorYalnızca çerezde olsaydı kullanıcı başka tarayıcıya geçtiğinde ya da çerezleri temizlediğinde tercihi kaybolurdu
Sütun ENUM, serbest metin değilVeritabanı, uygulamanın beyaz listesini kendisi de zorlar. Kod bir yerde doğrulamayı atlasa bile desteklenmeyen bir dil kodu tabloya yazılamaz
Varsayılan 'tr', NULL değilNULL "tercih yok" demek olurdu ve her okumada fazladan bir kontrol gerektirirdi
Çeviriler veritabanında değilÇeviri metni kod kadar sık değişir ve sürüm kontrolünde durmalıdır; her sayfa açılışında sorgu atmanın da anlamı yok
ENUM'un bedeli: yeni bir dil eklemek ALTER TABLE gerektirir. Bu bilinçli
bir ödünleşimdir — dil listesi yılda birkaç kez değişen, küçük ve kapalı bir
kümedir; buna karşılık veritabanı ikinci bir doğrulama katmanı olur. Dil
listeniz sık ve dinamik olarak değişecekse sütunu CHAR(5) yapıp doğrulamayı
tümüyle Translator::supports()'a bırakın.

Diğer tablolar panelin ortak altyapısıdır:

Tabloİşi
usersHesaplar · parola özeti · tema ve dil tercihi
remember_tokens"Beni hatırla" jetonları (özetlenmiş)
login_attemptsHız sınırı için deneme kayıtları
activity_logPanelde gösterilen son işlemler

SSS

Çevirileri veritabanında tutsam olmaz mı?

Olur ama önce şunu sorun: çeviri metni kim değiştirecek?

Cevap "geliştirici" ise dosya daha iyidir. Çeviri o zaman kodla birlikte sürümlenir, kod incelemesinden geçer, geri alınabilir ve her sayfa açılışında sorgu maliyeti doğurmaz.

Cevap "yönetici panelinden içerik ekibi" ise veritabanı gerekir. O durumda dosyaları önbellek katmanı gibi kullanın: veritabanından okuyup PHP dizisi olarak diske yazın, uygulama diskten okusun.

Bu projede birinci senaryo varsayıldı.

t() mi te() mi kullanmalıyım?

te() kullanın — çevirir ve HTML'e kaçışlar.

t() yalnızca metni döndürür. Bunu doğrudan echo ederseniz, çeviri dosyasına HTML girerse (veya yer tutucuya kullanıcı verisi geçerse) XSS açığı doğar.

t()'yi metni bir değişkene alıp işlemek gerektiğinde, te()'yi ekrana basarken kullanın. Kural basit: ekrana giden her şey te().

Arapça'nın altı çoğul kategorisi gerçekten gerekli mi?

Evet. Demo sayfasındaki tabloda 0, 1, 2, 5, 11 ve 100 için altı farklı karşılık görürsünüz. Bunlar süs değil, dilin dilbilgisi kuralıdır.

if ($n == 1) yazan bir kod Arapça'da 2 için tekil, 11 için çoğul üretir — ikisi de yanlıştır. Kuralı bilen tek yer ICU veri tabanıdır ve intl ona bakar.

intl eklentisi olmadan kullanabilir miyim?

Evet. Uygulama her yerelleştirme çağrısında eklentiyi denetler ve yoksa sade yedeğe düşer: çoğulda other biçimi, sayıda number_format(), tarihte date().

Site çalışır ama Arapça çoğul kuralları ve yerel para/tarih biçimleri doğru olmaz. Üretimde intl'i açmanızı öneririz; tek satırlık bir php.ini değişikliğidir.

Dil kodunu adresin içine koyabilir miyim? (/en/users)

Koyabilirsiniz ve SEO açısından genellikle daha iyidir. Bu projede sorgu parametresi seçildi çünkü örneğin odağı yönlendirme değil, çeviri katmanıdır.

Geçmek için: .htaccess'te ilk yol parçasını yakalayın, SUPPORTED listesindeyse $_GET['lang']'e yazıp yoldan düşürün. Translator::resolve() hiç değişmeden çalışmaya devam eder.

Çeviri anahtarını unutursam ne olur?

Ekranda anahtarın kendisi görünür: common.save.

Bu bilerek seçilmiştir. Boş metin dönseydi eksik çeviri sessizce kaybolur ve fark edilmezdi. Anahtar ise gözünüze batar. Yayına almadan önce anahtar denkliğini kontrol edin — üç dilde de 73 anahtar vardır.


Canlı Ortama Alırken

  • [ ] .env içinde APP_DEBUG=false (veya satırı tümüyle silin — ortamdan türetilir)
  • [ ] Veritabanı için root olmayan, yalnızca gereken yetkilere sahip bir kullanıcı açın
  • [ ] HTTPS zorunlu olsun; Session::isHttps() çerezi Secure yapacaktır
  • [ ] config/, app/, routes/, views/ klasörlerinin .htaccess dosyaları yerinde mi?
  • [ ] .env dosyasının tarayıcıdan erişilemediğini doğrulayın (403 dönmeli)
  • [ ] intl eklentisini açın
  • [ ] Demo hesaplarının parolalarını değiştirin veya hesapları silin
  • [ ] Üç dilde de anahtar denkliğini kontrol edin
  • [ ] Arapça'ya geçip düzenin aynalandığını gözle doğrulayın

Sorun Giderme

BelirtiSebepÇözüm
Ekranda common.save yazıyorO anahtar aktif dilde yoklang//common.php dosyasına anahtarı ekleyin
Çoğul her sayıda aynı çıkıyorintl kapalıphp.ini içinde extension=intl satırını açın
Arapça'da düzen aynalanmıyordir özniteliği basılmamışDüzen dosyasında ` var mı bakın
Sayı 1234567.89 görünüyorintl kapalı, yedek biçim çalışıyorintl'i açın
Dil değiştirince ana sayfaya atıyorDil seçici mevcut adresi korumuyorviews/partials/langswitch.php dosyasını kullanın
Tüm adresler 404mod_rewrite kapalıAçın veya .env içinde APP_PRETTY_URLS=false yapın
Giriş "çok fazla deneme" diyorHız sınırı devredelogin_attempts tablosunu boşaltın veya süreyi bekleyin
Sayfa boş / beyazPHP hatası, display_errors kapalı.env içinde APP_DEBUG=true` yapıp tekrar deneyin

Yol Haritası

  • [ ] Adres tabanlı dil yönlendirme (/en/users) örneği
  • [ ] Eksik çeviri anahtarlarını raporlayan CLI komutu
  • [ ] Çeviri dosyaları için .po / .mo içe aktarma köprüsü
  • [ ] Tarayıcı dilini ilk ziyarette öneren bir bildirim şeridi

Katkı

Hata bildirimi ve öneriler için issue açabilirsiniz. Yeni dil çevirisi gönderirseniz memnun oluruz — anahtar denkliğini koruyun ve çoğul kalıplarında o dilin kendi kategorilerini kullanın.

Lisans

MIT — ticari projelerinizde de özgürce kullanabilirsiniz.


Kaynak Kod soldaki ağaçtan bir dosya seçin

  • app
    • Core
      • Auth.php 13.9 KB
      • Autoloader.php 2.2 KB
      • Config.php 1.9 KB
      • Csrf.php 2.8 KB
      • Database.php 3.5 KB
      • Env.php 4.3 KB
      • Flash.php 2.6 KB
      • Middleware.php 2.5 KB
      • Paginator.php 8.6 KB
      • RateLimiter.php 3.9 KB
      • Request.php 4.5 KB
      • Response.php 7 KB
      • Router.php 6.3 KB
      • Session.php 6.3 KB
      • Translator.php 18.2 KB
      • Validator.php 8.8 KB
      • View.php 3.5 KB
    • Http
      • Controllers
        • Api
          • PreferenceApiController.php 2.7 KB
        • AuthController.php 3.2 KB
        • DashboardController.php 1.4 KB
        • I18nController.php 863 B
        • UserController.php 1.9 KB
      • Controller.php 1.3 KB
    • Models
      • User.php 6.8 KB
    • Repositories
      • ActivityRepository.php 3.8 KB
      • RememberTokenRepository.php 5.8 KB
      • UserRepository.php 7.3 KB
    • Support
      • helpers.php 15.9 KB
    • .htaccess 851 B
  • assets
    • css
      • admin.css 58.6 KB
      • bootstrap.min.css 227.5 KB
      • cilginyazilim.css 29.8 KB
      • feature.css 6.3 KB
    • images
      • logo.png 70.4 KB
    • js
      • app.js 20.2 KB
      • bootstrap.bundle.js 203.2 KB
      • jquery-3.7.0.js 278.3 KB
      • lang.js 1.1 KB
      • login.js 1.8 KB
      • users.js 4.7 KB
  • config
    • .htaccess 196 B
    • config.php 5 KB
    • menu.php 1.1 KB
  • docs
    • screenshots
      • 01-giris.png 372.4 KB
      • 02-kontrol-paneli.png 74.5 KB
      • 03-dil-ornekleri.png 151.2 KB
      • 04-arapca-rtl.png 93.6 KB
      • 05-kullanicilar.png 125.9 KB
      • 06-koyu-tema.png 152.3 KB
      • 07-mobil.png 85.4 KB
  • lang
    • ar
      • common.php 6.8 KB
    • en
      • common.php 5.3 KB
    • tr
      • common.php 6.2 KB
  • routes
    • .htaccess 197 B
    • web.php 3 KB
  • views
    • auth
      • login.php 7.9 KB
    • dashboard
      • _feature.php 2.2 KB
      • index.php 4 KB
    • errors
      • 404.php 844 B
      • 500.php 565 B
    • i18n
      • index.php 10.9 KB
    • layouts
      • admin.php 7.5 KB
      • auth.php 3.5 KB
      • plain.php 1.9 KB
    • partials
      • bottomnav.php 2.1 KB
      • head_extra.php 975 B
      • langswitch.php 2.7 KB
      • pagination.php 5.4 KB
      • sidebar.php 2.9 KB
      • topbar.php 3.9 KB
    • users
      • index.php 7.1 KB
    • .htaccess 262 B
  • .gitattributes 2.3 KB
  • .gitignore 1.2 KB
  • .htaccess 6 KB
  • CHANGELOG.md 6.4 KB
  • database.sql 14.9 KB
  • index.php 6.7 KB
  • LICENSE 1.1 KB
  • README.en.md 36.4 KB
  • README.md 37.7 KB
Dosya seçilmedi
İncelemek istediğiniz dosyayı soldaki ağaçtan seçin.

Güvenlik gereği kaynak dosyalardaki parola, API anahtarı ve benzeri gizli değerler gösterilmeden önce maskelenir (••••••••).

Sık Sorulan Sorular

Olur, ama önce şunu sorun: çeviri metnini kim değiştirecek? Cevap "geliştirici" ise dosya daha iyidir; çeviri kodla birlikte sürümlenir, kod incelemesinden geçer ve her sayfa açılışında sorgu maliyeti doğurmaz. Cevap "içerik ekibi, panelden" ise veritabanı gerekir. Bu projede birinci senaryo varsayılmıştır.

Evet. Demo sayfasındaki tabloda 0, 1, 2, 5, 11 ve 100 için altı farklı karşılık görürsünüz. Bunlar süs değil, dilin dilbilgisi kuralıdır. if ($n == 1) yazan bir kod Arapça'da 2 için tekil, 11 için çoğul üretir ve ikisi de yanlıştır.

Evet. Uygulama her yerelleştirme çağrısında eklentiyi denetler ve yoksa sade yedeğe düşer: çoğulda other biçimi, sayıda number_format(), tarihte date(). Site çalışmaya devam eder ama Arapça çoğul kuralları ve yerel biçimler doğru olmaz. Üretimde intl'i açmanızı öneririz; tek satırlık bir php.ini değişikliğidir.

Dört adım. lang/ altında klasörü açıp çeviriyi kopyalayın, değerleri çevirin, Translator::LOCALES dizisine dili ekleyin ve users.locale sütununu genişletin. Sağdan sola bir dilse yalnızca dir => rtl yazmanız yeterlidir; ayrı bir liste veya CSS dosyası yoktur.

Ekranda anahtarın kendisi görünür, örneğin common.save. Bu bilerek seçilmiştir: boş metin dönseydi eksik çeviri sessizce kaybolur ve fark edilmezdi. Anahtar ise gözünüze batar.

Koyabilirsiniz ve SEO açısından genellikle daha iyidir. Bu projede sorgu parametresi seçildi çünkü örneğin odağı yönlendirme değil çeviri katmanıdır. Geçmek için .htaccess'te ilk yol parçasını yakalayıp $_GET[lang] değerine yazmanız yeterlidir; Translator::resolve() hiç değişmez.

Yorumlar 0 konuşma

Bu kod örneğine henüz yorum yapılmamış. Takıldığınız bir yer veya merak ettiğiniz bir ayrıntı varsa ilk soruyu siz sorun.

Soru Sor veya Yorum Yaz

Yorumunuz onaylandıktan sonra yayınlanır. Teknik sorularınıza ekibimiz yanıt verir.

Yayınlanmaz; yalnızca yanıt bildirimi için kullanılır.
En az 10 karakter.

İlgili Kod Örnekleri