İçeriğe geç
Çılgın Yazılım

Uygulama İskeleti Orta

Ç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
php-multi-language-i18n/kunye.json
seviye
"Orta"
dosya
81
satir
~19.794
boyut
"2.3 MB"
veritabani
"cy_i18n"
lisans
"MIT"
inceleme
150
begeni
0
yayin
""

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 iş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?

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/<dil>/ 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.


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; }

<html dir="rtl"> 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.

<html> 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

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 · <html> 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/<dil>/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 <html dir="..."> 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 ağaçtan bir dosya seçin

multi-language-system
Dosya seçilmedi

İncelemek istediğiniz dosyayı 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

Çevirileri veritabanında tutsam olmaz mı?

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.

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 ve ikisi de yanlıştır.

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ış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.

Yeni bir dil eklemek ne kadar sürer?

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.

Çeviri anahtarını unutursam ne olur?

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.

Dil kodunu adresin içine koyabilir miyim?

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.

Bu örneğin daha gelişmişi projenizde mi lazım?

İhtiyacınızı anlatın; bu yapıyı sizin sisteminize uyarlayalım ya da size özel geliştirelim.

Projenizi Anlatın

Yorumlar

Henüz yorum yok. Takıldığınız bir yer varsa ilk soruyu siz sorun.

Soru sorun veya yorum yazın

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

Yayınlanmaz; yalnızca gerektiğinde size ulaşmak için.

En az 10 karakter.

İlgili kod örnekleri

Tüm kod örnekleri