Mert Özen Her Satırda Daha İleri
Frontend JavaScript Orta — #18

JavaScript İleri Tarih İşlemleri: Saat Dilimleri, UTC ve Intl ile Biçimlendirme

Mert Özen 3 Kas 2026 10 dk 5 görüntülenme
JavaScript İleri Tarih İşlemleri: Saat Dilimleri, UTC ve Intl ile Biçimlendirme

Date'in tek bir an tuttuğunu, UTC ve ISO 8601 kurallarını, Intl.DateTimeFormat seçeneklerini, Intl.RelativeTimeFormat ile "3 gün önce"yi, yaz saati tuzağını ve Temporal'a ön bakışı ele alıyoruz.

Sürüm notu: Bu yazıdaki kodlar ES2026 ve güncel tarayıcılar (Chrome, Edge, Firefox, Safari) içindir.

Önce Tahmin Et

İki tarih oluşturuyoruz. İkisi de "3 Kasım 2026, gece yarısı" gibi görünüyor; aralarındaki tek fark ikincisinin sonundaki T00:00:

const a = new Date("2026-11-03");
const b = new Date("2026-11-03T00:00");

console.log(a.getTime() === b.getTime());
console.log(a.toISOString());
console.log(b.toISOString());

Konsola ne yazar? Okumaya devam etmeden önce bir tahmin yap. İpucu: cevap, kodu dünyanın neresinde çalıştırdığına bağlı. Türkiye'deysen bir sonuç, Londra'daysan başka bir sonuç görürsün. Neden böyle olduğunu ISO 8601 bölümünde açıklayacağız.

Tek Bir An, Birçok Saat

Daha önce gördüğümüz Date object'inin içinde aslında tek bir sayı vardır: 1 Ocak 1970 UTC gece yarısından bu yana geçen milisaniye, yani timestamp. Saat dilimi, gün, ay gibi bilgilerin hiçbiri object'in içinde saklanmaz. Onları her sorduğunda motor bu sayıdan hesaplar.

Bir futbol maçının canlı yayınını düşün. Gol tek bir anda atılır. İstanbul'daki izleyici saatine bakınca 22:00, Londra'daki 19:00, New York'taki 14:00 görür. An tek, saatler farklı. Date o "an"ı tutar; yerel saat ise o anın bir saat diliminden nasıl göründüğüdür. getHours() anı tarayıcının saat dilimine göre, getUTCHours() ise UTC'ye göre okur:

const goal = new Date(Date.UTC(2026, 10, 3, 19, 0)); // 3 Kasım 19:00 UTC

goal.getTime();            // aynı an, tek bir sayı
goal.getUTCHours();        // 19
goal.getHours();           // İstanbul'da 22
goal.getTimezoneOffset();  // İstanbul'da -180

getTimezoneOffset'in işareti ters gelir: UTC'nin yerel saatten kaç dakika ileride olduğunu söyler, o yüzden UTC+3 için -180 döner. UTC, saat dilimlerinin sıfır noktasıdır ve yaz saatiyle hiç oynamaz. Sunucular, veritabanları ve API'ler bu yüzden UTC ile konuşur. Türkiye 2016'dan beri yıl boyunca UTC+3'te kalıyor, yani burada yaz saati geçişi yok. Ama kullanıcıların yurt dışında olabilir, sunucun başka bir ülkede durabilir; bu yazıdaki tuzaklar seni de bulur.

ISO 8601: Tarihin Ortak Dili

ISO 8601, tarihleri büyükten küçüğe yazan uluslararası bir biçimdir: 2026-11-03T19:00:00.000Z. Sondaki Z "UTC" demektir; onun yerine +03:00 gibi bir fark da yazılabilir. toISOString() her zaman UTC'de ve Z ile döner. Sıralanabilir, dilden bağımsızdır ve her sistem onu anlar.

Baştaki sorunun cevabı JavaScript'in bu biçimi çözerken uyguladığı bir kurala dayanıyor. Yalnızca tarih içeren bir ISO string ("2026-11-03") UTC olarak yorumlanır. Saat içeren ama sonunda Z ya da fark olmayan bir string ("2026-11-03T00:00") ise yerel saat olarak yorumlanır. İstanbul'da çalıştırdığında a UTC gece yarısıdır, b ise yerel gece yarısı, yani UTC'ye göre önceki gün 21:00:

// İstanbul'da çalıştırıldığında
a.getTime() === b.getTime(); // false
a.toISOString();             // "2026-11-03T00:00:00.000Z"
b.toISOString();             // "2026-11-02T21:00:00.000Z"

Londra'da kış saatinde, yani UTC ile aynı saatteyken, ikisi de aynı anı gösterir ve sonuç true olur. Aynı kod, farklı ülkede farklı cevap. Buradan çıkan kural basit: veriyi saklarken ve gönderirken her zaman UTC kullan (toISOString() ya da timestamp), parse ederken string'in sonunda Z ya da açık bir fark olsun. "03/11/2026" ya da "November 3, 2026" gibi ISO dışı biçimleri ise hiç parse ettirme; sonuç tarayıcıdan tarayıcıya değişebilir.

Intl.DateTimeFormat: Biçimlendirmenin Ayarları

Daha önce toLocaleDateString("tr-TR") ve Intl.DateTimeFormat ile ilk biçimlendirmeleri yaptık. Asıl güç seçeneklerde. En hızlı yol dateStyle ve timeStyle: "full", "long", "medium", "short" değerlerini alırlar. Daha ince ayar istiyorsan parçaları tek tek seçersin. Bunu konsola yapıştırıp dene:

const date = new Date(Date.UTC(2026, 10, 3, 9, 30));

new Intl.DateTimeFormat("en-US", { dateStyle: "full", timeStyle: "short" }).format(date);
// "Tuesday, November 3, 2026 at 12:30 PM" (İstanbul'da)

new Intl.DateTimeFormat("en-US", {
  weekday: "long", day: "numeric", month: "long", year: "numeric"
}).format(date);
// "Tuesday, November 3, 2026"

new Intl.DateTimeFormat("en-US", { dateStyle: "long", hour: "2-digit" });
// TypeError: dateStyle ile parça seçenekleri birlikte kullanılamaz

Son satır önemli bir kuralı gösteriyor: dateStyle/timeStyle ile hour, month gibi parça seçeneklerini karıştıramazsın. Ya hazır kalıbı seçersin ya da parçaları kendin. Çıktıdaki kelime sırası ve noktalama dile göre değişir; bunu sen değil, tarayıcının dil verisi belirler. Tarayıcıdan tarayıcıya ufak farklar da görebilirsin, bu yüzden biçimlendirilmiş metni asla sabit bir string'le karşılaştırma.

Üç yardımcı daha var. formatToParts, çıktıyı parça parça bir array olarak verir; ayı kalın göstermek gibi işler için birebir. formatRange, iki tarih arasını dile uygun biçimde yazar ve 2021'den beri bütün güncel tarayıcılarda var. resolvedOptions().timeZone ise kullanıcının saat dilimini söyler:

const fmt = new Intl.DateTimeFormat("en-US", { day: "numeric", month: "long", year: "numeric" });

fmt.formatToParts(date);
// [{ type: "month", value: "November" }, { type: "literal", value: " " }, { type: "day", value: "3" }, ...]

fmt.formatRange(new Date(2026, 10, 3), new Date(2026, 10, 7));
// "November 3 – 7, 2026"

Intl.DateTimeFormat().resolvedOptions().timeZone;
// "Europe/Istanbul"

Bir performans notu: bir formatter oluşturmak, onu kullanmaktan çok daha pahalıdır. Bin satırlık bir tabloyu biçimlendirirken döngünün içinde her seferinde new Intl.DateTimeFormat yazma; bir kez oluşturup format'ı tekrar tekrar çağır.

Başka Bir Saat Diliminde Göstermek

timeZone seçeneği, aynı anı istediğin saat diliminden göstermeni sağlar. Saat dilimleri "Europe/Istanbul", "America/New_York" gibi IANA isimleriyle yazılır. Bu isimler yaz saati kurallarını da bilir; "GMT+3" gibi sabit farklar bilmez. Farklı ülkelerdeki ekip arkadaşlarına toplantı saatini gösteren küçük bir örnek:

const meeting = new Date("2026-11-03T09:30:00Z");
const zones = ["Europe/Istanbul", "America/New_York", "Asia/Tokyo"];

for (const timeZone of zones) {
  const text = new Intl.DateTimeFormat("en-US", {
    dateStyle: "medium",
    timeStyle: "short",
    timeZone
  }).format(meeting);
  console.log(timeZone, text);
}
// Europe/Istanbul Nov 3, 2026, 12:30 PM
// America/New_York Nov 3, 2026, 4:30 AM
// Asia/Tokyo Nov 3, 2026, 6:30 PM

timeZoneName: "short" eklersen çıktının sonuna EST gibi bir etiket de gelir. Burada Date'in bir sınırına çarpıyoruz: başka bir saat diliminde göstermek kolay, ama "New York saatiyle 3 Kasım 09:00" diye bir tarih oluşturmanın yerleşik bir yolu yok. Date yalnızca UTC'yi ve tarayıcının kendi saat dilimini tanır. Bu boşluğu dolduran şeye yazının sonunda bakacağız.

Intl.RelativeTimeFormat: "3 Gün Önce"

Yorumların altındaki "5 dakika önce", bildirimlerdeki "dün" gibi ifadeler için Intl.RelativeTimeFormat var. 2020'den beri bütün güncel tarayıcılarda çalışıyor. Bir sayı ve bir birim verirsin, dile uygun cümleyi o kurar. numeric: "auto" seçeneği, dilin özel bir kelimesi varsa onu kullanır:

const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });

rtf.format(-1, "day");     // "yesterday"
rtf.format(1, "day");      // "tomorrow"
rtf.format(-3, "day");     // "3 days ago"
rtf.format(2, "hour");     // "in 2 hours"
rtf.format(-1, "week");    // "last week"

Dil olarak "tr" verirsen aynı satırlar "dün", "yarın", "3 gün önce" yazar. Negatif sayı geçmişi, pozitif sayı geleceği anlatır. Birimler "second", "minute", "hour", "day", "week", "month", "quarter" ve "year". Dikkat: RelativeTimeFormat farkı hesaplamaz, yalnızca yazar. Hangi birimi seçeceğine sen karar verirsin:

const rtfAuto = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
const units = [
  ["year", 60 * 60 * 24 * 365],
  ["month", 60 * 60 * 24 * 30],
  ["week", 60 * 60 * 24 * 7],
  ["day", 60 * 60 * 24],
  ["hour", 60 * 60],
  ["minute", 60],
  ["second", 1]
];

function timeAgo(date, now = new Date()) {
  const seconds = Math.round((date - now) / 1000);
  for (const [unit, size] of units) {
    if (Math.abs(seconds) >= size || unit === "second") {
      return rtfAuto.format(Math.round(seconds / size), unit);
    }
  }
}

console.log(timeAgo(new Date(Date.now() - 5 * 60 * 1000)));        // "5 minutes ago"
console.log(timeAgo(new Date(Date.now() - 3 * 24 * 60 * 60 * 1000))); // "3 days ago"

Ay ve yılı sabit gün sayısıyla hesaplamamız yaklaşık bir değer; "3 ay önce" gibi bir etiket için yeterli, ama bir kira sözleşmesinin bitiş tarihi için değil. Kesin takvim hesabı gereken yerde bir sonraki bölümün kurallarına uy.

Tarih Aralıkları ve Yaz Saati Tuzağı

İki tarih arasındaki gün sayısını bulmak için akla ilk gelen, milisaniye farkını bir günün milisaniyesine bölmektir. Çoğu zaman da çalışır. Yaz saatine geçilen gece ise çalışmaz, çünkü o gün 23 saat sürer. Aşağıdaki sonuçlar, bilgisayarı New York saat dilimindeyken alınmış:

// Tarayıcı America/New_York'tayken (8 Mart 2026'da yaz saatine geçiliyor)
const start = new Date(2026, 2, 7);
const end = new Date(2026, 2, 9);
console.log((end - start) / 86400000); // 1.9583333333333333

// Bir gün eklemek için 24 saat eklemek
const day = new Date(2026, 2, 8);
console.log(new Date(day.getTime() + 86400000).toString()); // 9 Mart 01:00, gece yarısı değil

// Takvim günü eklemek
day.setDate(day.getDate() + 1);
console.log(day.toString()); // 9 Mart 00:00

Kural şu: "kaç saat geçti" sorusu için milisaniye farkı doğrudur. "Kaç takvim günü" sorusu içinse ya setDate gibi takvim method'larını kullan ya da hesabı yaz saati olmayan UTC'de yap:

function daysBetween(a, b) {
  const utcA = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
  const utcB = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
  return Math.round((utcB - utcA) / 86400000);
}

daysBetween(new Date(2026, 2, 7), new Date(2026, 2, 9)); // 2, her saat diliminde

Bir de ay sonu tuzağı var. 31 Ocak'a bir ay eklersen Date 31 Şubat'ı arar, bulamaz ve taşan günleri Mart'a aktarır: new Date(2026, 0, 31) üzerinde setMonth(1) sonucu 3 Mart 2026 olur. Bu bir hata değil, Date'in tasarımı; ama kullanıcının beklediği de değil. Ay eklerken sonucun gününü kontrol etmen gerekir.

Temporal'a Ön Bakış

Bütün bu tuzakların ortak kökü Date'in tasarımı: değiştirilebilir bir object, tek bir sayı, yalnızca iki saat dilimi ve takvim hesabı için zayıf araçlar. TC39 bunun için yıllardır Temporal adında yeni bir API üzerinde çalışıyordu. Temporal 2026'da son aşamaya ulaştı ve ES2027'ye girmesi bekleniyor. Chrome, Edge ve Firefox'un güncel sürümlerinde çalışıyor, Safari henüz desteklemiyor; bugün her tarayıcıda kullanmak istersen bir polyfill gerekiyor. Destekleyen bir tarayıcının konsolunda dene:

Temporal.PlainDate.from("2026-01-31").add({ months: 1 }).toString();
// "2026-02-28" (taşma yok, ayın son günü)

Temporal.ZonedDateTime.from("2026-11-03T09:30[Europe/Istanbul]")
  .withTimeZone("America/New_York")
  .toString();
// "2026-11-03T01:30:00-05:00[America/New_York]"

Temporal.PlainDate.from("2026-11-03")
  .until("2026-12-25", { largestUnit: "months" })
  .toString();
// "P1M22D" (1 ay 22 gün)

Tarih ile saat ayrı tipler (PlainDate, PlainTime), saat dilimi tipin bir parçası (ZonedDateTime), süreler kendi tipine sahip (Duration) ve hiçbir object yerinde değişmiyor; her işlem yeni bir değer döndürüyor. Temporal'ı İleri seride ayrıntılı ele alacağız. O zamana kadar, Safari kullanıcılarının olduğu her yerde Date ve Intl ile bu yazıdaki kurallar geçerli.

Sık Düşülen Tuzaklar

Bugünün tarihini new Date().toISOString().slice(0, 10) ile almak. toISOString UTC'dir; İstanbul'da gece 00:00 ile 03:00 arasında bu satır sana dünün tarihini verir. Yerel YYYY-MM-DD için new Intl.DateTimeFormat("en-CA").format(new Date()) işini görür, çünkü bu dil o biçimi kullanır.

Date'in değiştirilebilir olduğunu unutmak. setDate, setMonth object'in kendisini değiştirir. Bir fonksiyona verdiğin tarihi orada değiştirirsen çağıran tarafın tarihi de değişir. Önce new Date(date) ile kopyala.

İki tarihi === ile karşılaştırmak. İkisi de object olduğu için aynı anı gösterseler bile false çıkar. a.getTime() === b.getTime() karşılaştır.

Ayların 0'dan başladığını unutmak. Daha önce gördüğümüz bu tuzak hâlâ burada: new Date(2026, 10, 3) Kasım'dır, Ekim değil.

Küçük Bir Deneme

★☆☆ Kendi doğum gününü Intl.DateTimeFormat ile üç farklı dilde ("tr-TR", "en-US", "de-DE") ve dateStyle: "full" ile yazdır. Kelime sırası ve noktalama nasıl değişiyor?

★★☆ Çıktıyı tahmin et, sonra dene: new Date("2026-11-03").getDate() İstanbul'da ne döner? Peki tarayıcın New York saat dilimindeyken ne dönerdi? İpucu: ISO bölümündeki kuralı hatırla.

★★★ timeAgo fonksiyonunu geliştir: bir haftadan eski tarihler için göreli zaman yerine Intl.DateTimeFormat ile "Nov 3, 2026" gibi kısa bir tarih göster, bir yıldan eski olanlara yılı da ekle. Formatter'ları fonksiyonun dışında bir kez oluştur.

Böylece Orta seriyi bitirdik. Modern syntax'tan closure'lara, promise'lerden Fetch'e, event'lerden module'lere kadar dilin günlük işte kullandığın her köşesini gördün. İleri seride kaputun altına iniyoruz: prototype zincirinin gerçekte nasıl işlediği, event loop'un microtask ve render adımlarıyla derinlemesine anatomisi, bellek yönetimi ve sızıntılar, performans ölçümü, ana thread'i rahatlatan Web Worker'lar, framework'süz yeniden kullanılabilir component'ler için Web Components ve bugün ön bakışını yaptığımız Temporal.