Ç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.
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?
- Depoyu klonlayın veya ZIP olarak indirip web kökünüze açın
- database.sql dosyasını içe aktarın (veritabanını kendisi oluşturur)
- .env.example dosyasını .env adıyla kopyalayıp veritabanı bilgilerinizi yazın
- php.ini içinde extension=intl satırını açın (önerilir, zorunlu değil)
- Tarayıcıdan açıp [email protected] / Admin1234 ile giriş yapın
Veritabanı şeması projedeki database.sql dosyasında.
Nasıl Çalışıyor?
Ç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.
🇹🇷 Türkçe · 🇬🇧 English
Canlı Demo
Kurulum yok, kayıt yok, indirme yok — tarayıcınızdan 3 saniyede deneyin.
▲ Görsele tıklayarak demoyu açabilirsinizDemo hesapları
| Rol | E-posta | Parola |
|---|---|---|
| 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 deneyin | Perde arkasında ne oluyor? |
|---|---|---|
| 1 | Üst çubuktaki dil seçicisinden EN seçin | Sayfa yenilenir ama bulunduğunuz sayfada kalırsınız. Dil seçici ana sayfaya atmaz; bulunduğunuz adrese ?lang= ekler |
| 2 | Şimdi AR seçin | 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. Tek bir "mobil sürüm" dosyası yok; CSS mantıksal özelliklerle yazıldığı için dir="rtl" yetiyor |
| 3 | Dil Örnekleri sayfasında "Çoğul kuralları" tablosuna bakın | 0, 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 |
| 4 | Aynı 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 okuyun | Hoş 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ın | Aynı 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ın | Dil seçiminiz giriş ekranından panele taşınır; hesabınıza yazılır. Çerezi silseniz bile tercihiniz kaybolmaz |
| 8 | Sağ üstteki ay/güneş simgesine basın | Koyu tema açılır ve hesabınıza kaydedilir. Tablo hücrelerinin kontrastı koyu temada da ölçülüdür (14,5:1) |
| 9 | Kullanıcılar sayfasında arama kutusuna ş yazın | Arama sunucuda çalışır; LIKE jokerleri kaçışlanır ve sıralama sütunu beyaz listeden doğrulanır |
| 10 | Telefonunuzdan açın | Kenar çubuğu alt navigasyona dönüşür; sayfa gövdesinde yatay kaydırma yoktur |
İpucu: Demoyu açıkken adres çubuğuna?lang=arekleyin. 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
| Konu | Durum |
|---|---|
| Veriler | database.sql içindeki 51 örnek kullanıcı. Gerçek kişi verisi yoktur; adresler @ornek.com uzantılıdır. |
| Sıfırlama | Demo veritabanı düzenli aralıklarla başlangıç hâline döner; yaptığınız değişiklikler kalıcı değildir. |
| Kimlik doğrulama | Vardır. Oturum, "beni hatırla" jetonu, hız sınırı ve CSRF korumasıyla birlikte gelir. |
APP_DEBUG | Canlıda kendiliğinden false — sunucu adından türetilir, yerelde true kalır. |
intl eklentisi | Demo sunucusunda açıktır. Kapalı olsaydı uygulama çalışmaya devam eder, yalnızca yerel biçimler sadeleşirdi. |
| Bağımlılık | Sı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
ifolmadığını öğrenmek isteyenler - Arapça, Farsça veya İbranice desteği verecek olanlar (RTL)
- Kütüphaneye bağlanmadan, PHP'nin kendi
intleklentisiyle 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
- Canlı Demo
- Bu Proje Nedir?
- Ekran Görüntüleri
- Kritik Kararlar
- Neler Var?
- Çeviri dört iştir
- Çoğul kuralları
- Dil nasıl seçilir?
- RTL desteği
- Yeni dil eklemek
- Güvenlik: Neyi, Nasıl Kapattık?
- Kurulum
- Yapılandırma
- Kendi Projenize Eklemek
- Dosya Yapısı
- Nasıl Çalışıyor?
- Veritabanı Şeması
- SSS
- Canlı Ortama Alırken
- Sorun Giderme
- Yol Haritası
- Katkı
- Lisans
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.

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.

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.

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.

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.

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).

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.
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ılanSı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 tutuculute()— çevir ve HTML'e kaçışlayarak basn()— ç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
intlile çoğul kuralları (ICU)NumberFormatterile sayı ve paraIntlDateFormatterile tarih ve saatintlyoksa 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ı:
$count | Türkçe | İngilizce | Arapça |
|---|---|---|---|
| 0 | Kayıt yok | No records | لا توجد سجلات |
| 1 | 1 kayıt | 1 record | سجل واحد |
| 2 | 2 kayıt | 2 records | سجلان |
| 5 | 5 kayıt | 5 records | 5 سجلات |
| 11 | 11 kayıt | 11 records | 11 سجلاً |
| 100 | 100 kayıt | 100 records | 100 سجل |
Dikkat edilecek üç nokta:
- Türkçe sütunu hiç değişmiyor (0 hariç). Türkçe'de sayıdan sonra çoğul eki gelmez.
- Arapça'da 2 satırı diğerlerine benzemiyor:
سجلانikili biçimdir, tekil de çoğul da değildir. - 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-left | margin-inline-start |
padding-right | padding-inline-end |
border-left | border-inline-start |
text-align: left | text-align: start |
left: 0 | inset-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.php2 · 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ızyeterlidir; 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şiminiVeritabanı Ş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çık | Tipik hatalı kod | Bu 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 enjeksiyonu | ORDER BY $_GET[sort] | Sütun adı beyaz listeden; yön yalnızca ASC/DESC |
| XSS | echo $user->name | Sunucuda e(), çeviride te(); ayrıca CSP script-src 'self' |
| CSRF | Gizli alan yok | Her 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 kuvvet | Sınırsız deneme | login_attempts tablosuyla e-posta + IP başına hız sınırı |
| Oturum çalma | Sabit oturum kimliği | Girişte kimlik yenilenir; çerez HttpOnly + SameSite=Lax + HTTPS'te Secure |
| Hata sızıntısı | Canlıda display_errors=On | APP_DEBUG ortamdan türetilir; gerçek alan adında kendiliğinden kapanır |
| Tıklama hırsızlığı | Başlık yok | X-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
| PHP | 8.0 veya üzeri |
| MySQL / MariaDB | 5.7+ / 10.3+ |
| Web sunucusu | Apache (mod_rewrite) veya Nginx |
| PHP eklentileri | pdo_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-system2 · Veritabanını içe aktarın
mysql -u root -p < database.sqlDosya 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=intlAçı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`;
| Karar | Neden |
|---|---|
| Dil tercihi hesapta tutuluyor | Yalnı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ğil | Veritabanı, 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ğil | NULL "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 eklemekALTER TABLEgerektirir. 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ütunuCHAR(5)yapıp doğrulamayı
tümüyleTranslator::supports()'a bırakın.
Diğer tablolar panelin ortak altyapısıdır:
| Tablo | İşi |
|---|---|
users | Hesaplar · parola özeti · tema ve dil tercihi |
remember_tokens | "Beni hatırla" jetonları (özetlenmiş) |
login_attempts | Hız sınırı için deneme kayıtları |
activity_log | Panelde 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
- [ ]
.enviçindeAPP_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()çereziSecureyapacaktır - [ ]
config/,app/,routes/,views/klasörlerinin.htaccessdosyaları yerinde mi? - [ ]
.envdosyasının tarayıcıdan erişilemediğini doğrulayın (403 dönmeli) - [ ]
intleklentisini 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
| Belirti | Sebep | Çözüm |
|---|---|---|
Ekranda common.save yazıyor | O anahtar aktif dilde yok | lang//common.php dosyasına anahtarı ekleyin |
| Çoğul her sayıda aynı çıkıyor | intl kapalı | php.ini içinde extension=intl satırını açın |
| Arapça'da düzen aynalanmıyor | dir özniteliği basılmamış | Düzen dosyasında ` var mı bakın |
Sayı 1234567.89 görünüyor | intl kapalı, yedek biçim çalışıyor | intl'i açın |
| Dil değiştirince ana sayfaya atıyor | Dil seçici mevcut adresi korumuyor | views/partials/langswitch.php dosyasını kullanın |
| Tüm adresler 404 | mod_rewrite kapalı | Açın veya .env içinde APP_PRETTY_URLS=false yapın |
| Giriş "çok fazla deneme" diyor | Hız sınırı devrede | login_attempts tablosunu boşaltın veya süreyi bekleyin |
| Sayfa boş / beyaz | PHP 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/.moiç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
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.