İçindekiler

GoUI, github.com/zatrano/goui/i18n içinde küçük, bağımlılığı olmayan bir çevirmen (translator) sunar. Kasıtlı olarak basittir: locale başına düz JSON key/value dosyaları, bir fallback locale, ve eksik key'ler için görünür bir yer tutucu — böylece bozuk bağlantılar (wiring) sessizce boş bir string olarak render edilmez.

import "github.com/zatrano/goui/i18n"

1. Translator kurulumu

type Translator struct { /* ... */ }

func NewTranslator() *Translator
func (t *Translator) LoadLocale(locale string, filePath string) error
func (t *Translator) Translate(locale, key string, args ...any) string

Uygulama başına bir Translator oluşturun (oturum başına değil, bileşen başına değil) ve her yerde paylaşın:

tr := i18n.NewTranslator()
if err := tr.LoadLocale("tr", filepath.Join(root, "i18n", "locales", "tr.json")); err != nil {
    log.Fatal(err)
}
if err := tr.LoadLocale("en", filepath.Join(root, "i18n", "locales", "en.json")); err != nil {
    log.Fatal(err)
}

Ardından bunu ws.NewServer'a verin — bu sunucu tarafından oluşturulan her oturum aynı *Translator örneğini alır, ve ws.Session her mount edilen bileşende otomatik olarak SetTranslator'ı çağırır:

server := ws.NewServer(hub, registry, tr)
gouifiber.Register(app, gouifiber.Options{Server: server})

Alt alanları kendiniz inşa ediyorsanız (örn. bir üst form struct'ı içindeki Tier 1 girdileri), her birinde açıkça SetTranslator'ı çağırın, çünkü oturum yalnızca üst seviye bileşenin kendi core.BaseComponent'ine reflection yapar:

c := &ContactForm{ /* ... */ }
c.SetTranslator(tr)
c.Name.SetTranslator(tr)
c.Email.SetTranslator(tr)

Yüklenmiş locale'i olmayan boş bir i18n.NewTranslator() tamamen geçerlidir — Translate her zaman [[key]] yer tutucusuna geri döner, bu gerçek metne ihtiyaç duymayan örnekler/demolar için uygundur (bkz. examples/counter, examples/searchable-select vb., ki bunlar ws.NewServer(...)'a düz bir i18n.NewTranslator() geçirir).

2. JSON dosya formatı

Locale dosyaları düz JSON nesneleridir: string key'ler string değerlere eşlenir. LoadLocale açıkça iç içe (nested) JSON nesnelerini desteklemez"form.submit" gibi bir key, içindeki nokta yüzünden sadece namespace'lenmiş görünen tek, düz bir map key'idir, {"form": {"submit": "..."}} gibi iç içe bir nesne yolu değildir.

{
  "welcome_message": "Welcome, {{.Name}}",
  "form.required_field": "This field is required",
  "form.submit": "Submit",
  "form.cancel": "Cancel",
  "nav.home": "Home",
  "validation.required": "This field is required",
  "validation.email": "Please enter a valid email address"
}

Kod tabanının "iç içe-düz key'ler" ("nested-flat keys") ile kastettiği şey budur: noktalı, hiyerarşik görünen key isimleri (form.required_field, validation.email, forms.password_strength.weak), gerçekten iç içe bir JSON ağacı olarak değil, düz bir map[string]string olarak saklanır. Gerçekten iç içe bir JSON belgesi ({"form": {"submit": "Submit"}}) yüklemek, LoadLocale'in beklediği map[string]string'e json.Unmarshal edilirken başarısız olur — her locale dosyasını bir seviye derinlikte tutun.

Çatının kendi paketlenmiş locale'leri i18n/locales/tr.json ve i18n/locales/en.json'da yaşar ve Tier 1/Tier 2 kontrolleri tarafından kutudan çıktığı gibi kullanılan key'leri kapsar (validation.*, forms.password_strength.*, forms.date_range.invalid, forms.time_range.invalid, forms.otp.incomplete, vb.). Bunları (veya kendi kopyalarınızı) yükleyin, böylece bu yerleşik hata mesajları [[validation.required]] olarak değil çevrilmiş olarak render edilir.

Yer tutucular

Değerler Go text/template yer tutucuları içerebilir. Translate, eşleşen string'i bir şablon olarak yeniden ayrıştırır ve geçirdiğiniz ilk variadic argümana karşı yürütür:

tr.Translate("tr", "welcome_message", map[string]any{"Name": "Serhan"})
// → "Hoş geldin, Serhan"

Herhangi bir sebeple interpolasyon başarısız olursa (JSON değerinde kötü şablon sözdizimi, eşleşmeyen alan adı vb.), Translate hata vermek yerine ham (interpolasyon yapılmamış) string'i döndürür.

3. Arama ve fallback: önce tr, sonra [[key]]

Translate(locale, key, args...) şu sırayla çözülür:

  1. key'i locale için yüklenen mesajlarda arayın.
  2. Bulunamazsa (locale hiç yüklenmemiş ya da key ondan eksik), ve locale != i18n.BaseLocale ise, key'i i18n.BaseLocale ("tr" — şu anda Türkçe'ye sabit kodlanmış sabit) için yüklenen mesajlarda arayın.
  3. Hâlâ bulunamazsa, "[[" + key + "]]" literal string'ini döndürün.
const BaseLocale = "tr"

Bu şu anlama gelir:

  • Desteklenmeyen/bilinmeyen bir locale (mesela hiç yüklenmemiş "de"), o key için "tr"'de her ne varsa şeffaf bir şekilde ona geri döner.
  • Desteklenen ama sadece bir key'i eksik olan bir locale (örn. "en" yüklendi ama biri "form.cancel"'ı eklemeyi unuttu) da o tek key için "tr" değerine geri döner, boş bir string'e değil.
  • Hem istenen locale'de hem de "tr"'de bulunmayan bir key, "[[the.missing.key]]" olarak render edilir — UI'de görünür, bir demo veya QA sırasında grep'lemesi kolay, ve gerçek metinle karışması imkansız.
tr.Translate("de", "form.submit")       // "Gönder" — tr'ye geri döner
tr.Translate("tr", "does.not.exist")    // "[[does.not.exist]]"

BaseComponent.T, bunun üzerine bir fallback katmanı daha ekler: bir bileşene hiç çevirmen enjekte edilmemişse (SetTranslator hiç çağrılmadıysa), T, arama yapmayı denemeden bile hemen "[[" + key + "]]" döndürür — böylece eksik bağlantı ve eksik key'ler aynı ve eşit derecede belirgin görünür.

4. Bir bileşenden çeviri kullanma

func (c *MyComponent) Render() (string, error) {
    label := c.T("form.submit")                       // yer tutucu yok
    greeting := c.T("welcome_message", map[string]any{"Name": "Ada"}) // yer tutucuyla
    return "<button>" + html.EscapeString(label) + "</button>" +
        "<p>" + html.EscapeString(greeting) + "</p>", nil
}

Veya, core.RenderTemplate (otomatik olarak html/template ile escape eden) üzerinden render ederken, T'yi veride geçirin ve {{call .T "key" .}}'yi kullanın — tam yer tutucu kuralları için 02-components.md'ye bakın.

core.BaseComponent üzerindeki Locale, T'nin hangi locale'i kullanacağını seçer; WebSocket URL'sindeki ?locale= sorgu string'i parametresinden ayarlanır (new GoUIClient('/goui/ws', 'counter', { locale: 'tr' })) ve ws.Session bir bileşeni mount veya activate ettiğinde yayılır. Locale boşsa, T, i18n.BaseLocale'i ("tr") kullanır.

5. Yeni bir dil ekleme

  1. tr.json/en.json ile aynı düz key kümesine sahip i18n/locales/<code>.json (veya kendi locale dosyalarınızı tuttuğunuz herhangi bir yer) oluşturun — en azından, yerleşik Tier 1/Tier 2 kontrollerinin dayandığı validation.* ve forms.* key'leri, artı kendi uygulama key'leriniz.

  2. Başlangıçta diğerleriyle birlikte yükleyin:

    tr := i18n.NewTranslator()
    _ = tr.LoadLocale("tr", filepath.Join(root, "i18n", "locales", "tr.json"))
    _ = tr.LoadLocale("en", filepath.Join(root, "i18n", "locales", "en.json"))
    _ = tr.LoadLocale("fr", filepath.Join(root, "i18n", "locales", "fr.json"))
    
  3. İstemcinin locale: "fr" ile bağlanmasını sağlayın:

    new GoUIClient('/goui/ws', 'contact', { mount: '#app', locale: 'fr' }).connect();
    
  4. fr.json'da henüz çevirmediğiniz herhangi bir key, sayfayı bozmak yerine sessizce tr (temel locale) değerine geri döner — hangi zaman uygun olursa doldurun, hiçbir build adımı veya kod değişikliği gerekmez.

i18n.BaseLocale'i değiştirmek için şu anda bir çalışma zamanı API'si yoktur — bu, derleme zamanı bir sabittir ("tr"). Uygulamanızın kanonik dili Türkçe değilse, birincil/varsayılan locale dosyanızın hâlâ tam kapsamına sahip olduğundan emin olun, çünkü o, diğer her locale için fallback hedefidir.