docs: add multilingual i18n rollout plan

This commit is contained in:
poyrazavsever
2026-07-19 03:06:30 +03:00
parent bace8c86cf
commit 0548820584
11 changed files with 2498 additions and 0 deletions
@@ -0,0 +1,171 @@
---
title: Faz 0 ADR Seti
description: Neta cok dilli sisteminin baslangic karar kayitlari.
phase: 0
status: accepted
last_updated: 2026-07-19
---
# Faz 0 ADR Seti
Bu dosya cok dillilik calismasinin ilk uygulama kararlarini kilitler. Amac,
sonraki fazlarda ayni problemi tekrar tekrar tartismadan ilerleyebilmek ve
SQLite tabanli self-host hedefini korumaktir.
## ADR-I18N-0001 — UI cevirisi ve domain icerik cevirisi ayrilacak
### Durum
Kabul edildi.
### Baglam
Neta'da iki farkli metin tipi var:
- Uygulamanin kendisine ait sabit arayuz metinleri.
- Freelancer veya musteri tarafindan uretilen is icerikleri.
Bu iki veri tipini ayni tabloda veya ayni anahtar modeliyle tutmak, hem fallback
hem de izin modeli tarafinda karmasa yaratir.
### Karar
Arayuz cevirileri stabil katalog anahtarlariyla yonetilecek. Built-in `tr` ve
`en` kataloglari kod icinde surumlenecek; instance sahibi tarafindan eklenen
override'lar SQLite icindeki `instance_ui_translations` modelinde tutulacak.
Domain icerik cevirileri entity ve field bazli tutulacak. Bunun icin Faz-1'de
`content_translations` modeli eklenecek.
### Sonuc
- Arayuz metinleri release sureciyle denetlenebilir kalir.
- Sonradan eklenen diller deployment gerektirmeden doldurulabilir.
- Is icerikleri icin form tab'lari net bicimde modellenir.
- Chat, gunluk notu ve musteri mesajlari gibi iletisim icerikleri otomatik
cevrilmez; yazildigi dilde kalir.
## ADR-I18N-0002 — Locale cozumleme onceligi
### Durum
Kabul edildi.
### Baglam
Ayni instance icinde freelancer, musteri portali, davet ekranlari ve public API
farkli baglamlarda calisir. Locale cozumleme her yerde ayni kaynaga bakarsa
portal daveti gibi auth oncesi akislarda yanlis dil gorunebilir.
### Karar
Locale asagidaki sirayla cozumlenecek.
Auth oncesi davet ekranlari:
1. Davet kaydindaki locale.
2. Guvenli locale cookie'si.
3. Instance varsayilan dili.
4. Built-in fallback `tr`.
Freelancer uygulamasi:
1. Oturum acmis kullanicinin `user_preferences.language` degeri.
2. Guvenli locale cookie'si.
3. Instance varsayilan dili.
4. Built-in fallback `tr`.
Musteri portali:
1. Musteri profilinin veya `clients.portal_locale` alaninin locale degeri.
2. Portal davetinden gelen locale.
3. Guvenli locale cookie'si.
4. Instance varsayilan dili.
5. Built-in fallback `tr`.
Mobil ve API istekleri:
1. Auth kullanicisinin kayitli tercihi.
2. Guvenilir endpoint parametresi veya `Accept-Language` sinyali.
3. Instance varsayilan dili.
4. Built-in fallback `tr`.
### Sonuc
- Musteri davet ekrani login olmadan dogru dilde acilir.
- Freelancer tercihi musteri portalini yanlislikla etkilemez.
- Mobil istemci ayni kurallari API kontratindan okuyabilir.
## ADR-I18N-0003 — Dashboard ve portal route'larina locale prefix eklenmeyecek
### Durum
Kabul edildi.
### Baglam
`/tr/dashboard`, `/en/dashboard` gibi route prefix'leri public marketing
sitelerinde gucludur, ancak Neta'nin dashboard ve portal yapisi oturum, davet,
yetki ve self-host basitligi uzerine kurulu. Prefix eklemek route sayisini,
redirect davranislarini ve mobil endpoint eslemesini gereksiz buyutur.
### Karar
Dashboard, portal ve davet route'lari mevcut URL yapisini koruyacak. Dil, locale
resolver ve cookie/user/client kayitlariyla belirlenecek.
### Sonuc
- Var olan internal linkler ve mobil baglanti modeli korunur.
- Self-host deploy ve reverse proxy ayarlari sade kalir.
- Public landing/docs tarafi isterse ileride ayri route prefix stratejisi
kullanabilir; bu karar sadece uygulama kabugu icindir.
## ADR-I18N-0004 — Hafif custom runtime i18n katmani yazilacak
### Durum
Kabul edildi.
### Baglam
Projenin ana hedeflerinden biri dependency sayisini azaltmak ve self-host'u
kolaylastirmak. Mevcut ihtiyaclar temel katalog cozumleme, fallback, namespace
yukleme, `Intl` bicimlendirme ve SQLite override okuma uzerinden karsilanabilir.
### Karar
Ilk cok dillilik surumunde agir bir i18n runtime dependency'si eklenmeyecek.
Neta icinde kucuk bir i18n katmani yazilacak:
- `lib/i18n/*` ortak tip ve format yardimcilari.
- `server/i18n/*` locale resolver, katalog merge ve translator servisleri.
- Client component'ler icin sinirli scope'ta provider.
- Tarih, sayi ve para bicimlendirme icin native `Intl`.
### Sonuc
- Yeni dependency eklenmeden Faz-2 runtime kurulabilir.
- Katalog sozlesmesi Neta'nin domain ihtiyaclarina gore sekillenir.
- Ileride ihtiyac buyurse baska kutuphaneye gecis icin katalog anahtarlari
korunabilir.
## ADR-I18N-0005 — Built-in diller ve yayin kurali
### Durum
Kabul edildi.
### Karar
Turkce (`tr`) ve Ingilizce (`en`) built-in, aktif ve silinemez diller olacak.
Instance varsayilan dili ilk migration sonrasinda `tr` olacak.
Yeni eklenen diller once `draft` durumunda olusturulacak. Portal icin kritik
namespace'leri tamamlanmayan bir dil musteri dili olarak secilemeyecek.
### Sonuc
- Eksik ceviri musteriye yarim bir portal deneyimi olarak yansimaz.
- Instance sahibi yeni dili hazirlayip test ettikten sonra aktif edebilir.
- Fallback davranisi her zaman tahmin edilebilir kalir.
@@ -0,0 +1,44 @@
---
title: Faz 0 Baseline Sonuclari
description: Cok dillilik calismasi baslamadan onceki build, typecheck ve smoke referansi.
phase: 0
status: completed
last_updated: 2026-07-19
---
# Faz 0 Baseline Sonuclari
Bu baseline, cok dillilik kod degisikliklerinden onceki saglik referansidir.
Sonraki fazlarda yeni hata olup olmadigini anlamak icin bu komut setiyle
karsilastirma yapilacak.
## Komut sonuclari
| Komut | Sonuc | Not |
| --- | --- | --- |
| `pnpm typecheck` | Basarili | `tsc --noEmit` hatasiz tamamlandi. |
| `pnpm build` | Basarili | Next.js 16.2.10 production build ve standalone hazirlama tamamlandi. |
| `pnpm phase9:smoke` | Basarili | API boundary, auth/invitation ve mobile API smoke kapilari gecti. |
## Build notu
`pnpm build` sirasinda su Next.js uyarisi goruldu:
```text
Using edge runtime on a page currently disables static generation for that page
```
Bu Faz-0 icin yeni bir hata degil; mevcut route/runtime tercihinden kaynaklanan
uyari olarak kaydedildi.
## Faz-1 icin baseline beklentisi
Faz-1 sonunda en az asagidaki komutlar tekrar calistirilmelidir:
- `pnpm typecheck`
- `pnpm build`
- Locale migration eklendikten sonra ilgili DB smoke komutu
- Varsa yeni `i18n` service unit/smoke komutu
Faz-5 ve Faz-6 gibi domain/portal davranisi degisen fazlarda `phase6:smoke` ve
portal-specific fixture testleri de baseline setine eklenmelidir.
@@ -0,0 +1,181 @@
---
title: Faz 0 I18n Fixture Plani
description: Cok dillilik migration, katalog ve portal testleri icin seed senaryolari.
phase: 0
status: completed
last_updated: 2026-07-19
---
# Faz 0 I18n Fixture Plani
Bu plan Faz-1 sonrasinda script veya test fixture'ina donusturulecek veri setini
tanımlar. Hedef, locale resolver, fallback, domain icerik cevirileri ve portal
dili davranisini ayni senaryoda dogrulamaktir.
## Locale fixture'lari
| Locale | Durum | Built-in | Fallback | Amac |
| --- | --- | --- | --- | --- |
| `tr` | `active` | Evet | Yok | Instance default ve mevcut verinin ana dili. |
| `en` | `active` | Evet | `tr` | Built-in ikinci dil ve portal client testi. |
| `fr` | `draft` | Hayir | `en` | Eksik katalog/fallback testi. |
| `ar-XB` | `test` | Hayir | `en` | RTL smoke ve layout kontrolu. |
`tr` ve `en` silinemez. `fr` portal dili olarak ancak kritik portal namespace'leri
tamamlaninca secilebilir. `ar-XB` urun ayarlarinda normal kullaniciya
gosterilmez.
## Katalog fixture'lari
### Turkce built-in katalog
Tum namespace'lerde eksiksiz olmalidir:
- `common`
- `auth`
- `navigation`
- `dashboard`
- `clients`
- `projects`
- `tasks`
- `calendar`
- `finance`
- `journal`
- `chat`
- `settings`
- `portal`
- `status`
- `validation`
- `api`
### Ingilizce built-in katalog
Turkce ile ayni anahtar setine sahip olmalidir. Faz-2 testinde anahtar farki
release engeli kabul edilir.
### Eksik Fransizca katalog
Fallback test etmek icin bilincli olarak kismi tutulur.
Ornek:
```json
{
"navigation.dashboard": "Tableau de bord",
"navigation.projects": "Projets",
"portal.dashboard.title": "Apercu",
"common.save": "Enregistrer"
}
```
Beklenen davranis: Eksik `portal.tasks.title` gibi anahtarlarda once `en`, sonra
`tr` fallback metni gosterilir; kullaniciya ham anahtar basılmaz.
## Auth fixture'lari
| Kullanici | Rol | Email | Dil | Baglam |
| --- | --- | --- | --- | --- |
| Freelancer | `admin` | `owner@neta.local` | `tr` | Dashboard, ayarlar ve ceviri yonetimi. |
| Turkce musteri | `client` | `client-tr@neta.local` | `tr` | Portal default dil testi. |
| Ingilizce musteri | `client` | `client-en@neta.local` | `en` | Portal client locale testi. |
Freelancer hesabi instance default dilini `tr` olarak korur. Ingilizce musteri
icin davet kaydinda ve `clients.portal_locale` alaninda `en` beklenir.
## Domain veri fixture'i
### Musteriler
| ID | Ad | Portal dili | Not |
| --- | --- | --- | --- |
| `client-tr` | Ada Yilmaz | `tr` | Turkce portal goruntusu. |
| `client-en` | Nova Studio | `en` | Ingilizce portal goruntusu. |
### Projeler
| ID | Musteri | Default baslik | EN baslik | FR baslik | Not |
| --- | --- | --- | --- | --- |
| `project-website` | `client-tr` | Web sitesi yenileme | Website refresh | Bos | Fallback testi. |
| `project-brand` | `client-en` | Marka kiti | Brand kit | Kit de marque | Portal EN ve FR content testi. |
Beklenen davranis:
- `tr` isteyen kullanici default kolon veya `content_translations(tr)` degerini
gorur.
- `en` isteyen kullanici EN content translation'i gorur.
- `fr` isteyen kullanici FR alan bos ise fallback zinciriyle EN veya TR gorur.
### Gorevler
| ID | Proje | Public | TR baslik | EN baslik |
| --- | --- | --- | --- | --- |
| `task-wireframe` | `project-website` | Evet | Ana sayfa wireframe | Homepage wireframe |
| `task-internal` | `project-website` | Hayir | Ic notlari toparla | Gather internal notes |
Portal yalnizca `is_public_to_client = true` gorevleri gosterir.
### Planlama bolumleri
| ID | Proje | Kategori | TR baslik | EN baslik |
| --- | --- | --- | --- | --- |
| `plan-scope` | `project-website` | `scope` | Kapsam | Scope |
| `plan-delivery` | `project-website` | `delivery` | Teslimat | Delivery |
Kategori teknik degeri cevrilmez; label `status` veya `projects` namespace'inden
gelir.
## Portal davet fixture'lari
| Davet | Email | Locale | Beklenen ekran dili |
| --- | --- | --- | --- |
| `invite-tr` | `client-tr@neta.local` | `tr` | Turkce |
| `invite-en` | `client-en@neta.local` | `en` | Ingilizce |
| `invite-fr-draft` | `client-en@neta.local` | `fr` | Portal publish kurali nedeniyle reddedilir veya fallback EN kullanir. |
Davet ekranlari auth oncesi oldugu icin locale'i session'dan degil davet
kaydindan alir.
## API contract fixture'lari
`/api/v1/meta` Faz-8 sonunda en az su localization bilgisini dondurmelidir:
```json
{
"localization": {
"defaultLocale": "tr",
"supportedLocales": [
{ "code": "tr", "name": "Turkce", "status": "active", "builtIn": true },
{ "code": "en", "name": "English", "status": "active", "builtIn": true },
{ "code": "fr", "name": "Francais", "status": "draft", "builtIn": false }
],
"fallbacks": {
"en": "tr",
"fr": "en"
}
}
}
```
Mobil istemci aktif olmayan dilleri kullanici seciminde gostermemeli, ancak
ceviri yonetimi ekranlari icin admin API'de draft dilleri gorebilmelidir.
## Smoke beklentileri
Faz-2 sonrasinda:
- `t("common.save")` `tr` icin `Kaydet`, `en` icin `Save` dondurur.
- `fr` locale'inde eksik anahtar ham key olarak gosterilmez.
- `formatMoney(10000, "TRY", "tr")` Turkce bicim verir.
- `formatDate(..., "en")` Ingilizce ay adi verir.
Faz-6 sonrasinda:
- `client-en` portali EN navigasyon ve EN proje basliklariyla acilir.
- `client-tr` portali TR kalir.
- Davet kabul edildikten sonra secilen locale client kaydina yansir.
Faz-8 sonrasinda:
- `/api/v1/meta` supported locale listesini dondurur.
- `/api/v1/me` kullanicinin etkin locale'ini ve fallback chain'ini dondurur.
- API hata response'lari stabil kod + locale'e uygun insan okunur mesaj tasir.
@@ -0,0 +1,149 @@
---
title: Faz 0 I18n Envanteri
description: Kullaniciya gorunen metinler, locale sabitleri ve namespace migration envanteri.
phase: 0
status: completed
last_updated: 2026-07-19
---
# Faz 0 I18n Envanteri
Bu envanter Faz-1 ve Faz-2 icin kapsam sinirini belirler. Liste uygulama
kodunun mevcut durumuna gore hazirlandi; yeni sayfa eklendikce ayni namespace
modeliyle genisletilmelidir.
## Namespace standardi
Katalog anahtarlari `namespace.section.item` biciminde olacak. Turkce cumleler
anahtar olarak kullanilmayacak.
| Namespace | Sahip oldugu alan |
| --- | --- |
| `common` | Kaydet, iptal, sil, ara, filtrele, yukleniyor, bos durum, toast eylemleri |
| `auth` | Login, register, setup, davet kabul, auth hata mesajlari |
| `navigation` | Freelancer ve portal sidebar grup/link label'lari |
| `dashboard` | Freelancer ana sayfa basliklari, stats, grafik ve son kayit kartlari |
| `clients` | Musteri liste, detay, form, portal hesap islemleri |
| `projects` | Proje liste, detay, form, planlama, revizyon ve risk analizi |
| `tasks` | Gorev liste, kanban, form, durum ve oncelik UI metinleri |
| `calendar` | Takvim, etkinlik kartlari, etkinlik formu |
| `finance` | Finans stats, islem formu, kategori, AI analiz modal'i |
| `journal` | Gunluk liste, form, mood/enerji label'lari |
| `chat` | Sohbet listesi, bos durum, hata ve input metinleri |
| `settings` | Genel gorunum, marka, AI, profil, dil yonetimi |
| `portal` | Musteri portal dashboard, projeler, gorevler, revizyonlar |
| `status` | Enum label'lari: status, priority, type, pipeline, payment |
| `validation` | Form/action hata ve basari mesajlari |
| `api` | Mobil/public API hata kodlari icin insan okunur mesajlar |
## Freelancer migration listesi
Faz-4'te asagidaki sirayla UI sabitleri katalog anahtarina alinacak.
| Sira | Route veya dosya | Namespace'ler | Not |
| --- | --- | --- | --- |
| 1 | `config/sidebar.ts` | `navigation` | Sidebar link ve grup adlari merkezi baslangic noktasi. |
| 2 | `components/layout/app-shell.tsx` | `navigation`, `common`, `settings` | Account menu, tema ve layout metinleri. |
| 3 | `app/(dashboard)/page.tsx`, `dashboard-client.tsx` | `dashboard`, `common`, `status` | Ana stats, grafik bos durumlari ve tarih/para formatlari. |
| 4 | `app/(dashboard)/clients/*` | `clients`, `common`, `status`, `validation` | Liste, detay, portal hesabi, activity mesajlari. |
| 5 | `app/(dashboard)/projects/*` | `projects`, `common`, `status`, `validation` | Proje formu, planlama, revizyon ve risk analizi. |
| 6 | `app/(dashboard)/tasks/*` | `tasks`, `common`, `status`, `validation` | Liste/kanban ayrimi ve gorev action metinleri. |
| 7 | `app/(dashboard)/calendar/*` | `calendar`, `common`, `status` | Date formatting Faz-2 format yardimcilarina tasinir. |
| 8 | `app/(dashboard)/finance/*` | `finance`, `common`, `status`, `validation` | Para formatting ve AI modal metinleri. |
| 9 | `app/(dashboard)/journal/*` | `journal`, `common`, `validation` | Mood label'lari ve date formatter'lar. |
| 10 | `app/(dashboard)/chat/*`, `app/api/chat/route.ts` | `chat`, `api`, `validation` | Kullaniciya donen hata mesaji daha detayli hale getirilmisti; i18n anahtariyla baglanacak. |
| 11 | `app/(dashboard)/settings/*` | `settings`, `common`, `validation` | Dil yonetimi Faz-3'te eklenecegi icin en son genisletilir. |
| 12 | `app/(dashboard)/analytics/*`, `business/*` | `dashboard`, `finance`, `common` | Mevcut business ekranlari i18n kapsamina alinacak. |
## Portal migration listesi
Portal Faz-6'da musteri diliyle birlikte ele alinacak.
| Sira | Route veya dosya | Namespace'ler | Not |
| --- | --- | --- | --- |
| 1 | `config/portal-sidebar.ts` | `navigation`, `portal` | Portal navigasyon label'lari. |
| 2 | `components/layout/portal-shell.tsx` | `portal`, `navigation`, `common` | Kullanici menu ve kabuk metinleri. |
| 3 | `app/portal/page.tsx` | `portal`, `dashboard`, `common` | Musteri dashboard stats ve bos durumlari. |
| 4 | `app/portal/projects/page.tsx` | `portal`, `projects`, `status` | Proje liste metinleri ve tarih formatlari. |
| 5 | `app/portal/projects/[id]/*` | `portal`, `projects`, `tasks`, `status`, `validation` | Cevrilebilir project/task content resolver kullanir. |
| 6 | `app/portal/tasks/page.tsx` | `portal`, `tasks`, `status` | Public gorev metinleri. |
| 7 | `app/portal/revisions/page.tsx` | `portal`, `projects`, `validation` | Revizyon talepleri yazildigi dilde kalir. |
| 8 | `app/invite/[token]/*` | `auth`, `portal`, `validation` | Auth oncesi davet locale'iyle render edilir. |
## Auth ve API migration listesi
| Alan | Dosya | Namespace | Not |
| --- | --- | --- | --- |
| Admin setup | `app/register/page.tsx`, `app/login/*` | `auth`, `validation` | Cift toast fix'i korunarak metinler kataloglanir. |
| Better Auth route | `app/api/auth/[...all]/route.ts` | `api`, `auth` | Kullaniciya acik hata mapping'i gerekir. |
| Portal invitations API | `app/api/portal-invitations/*` | `api`, `portal` | Locale parametresi Faz-6'da kontrata eklenir. |
| Mobile API v1 | `app/api/v1/*`, `server/api/v1/*` | `api`, `common` | Meta endpoint supported/default locale bilgisini dondurur. |
| Branding API | `app/api/branding/*` | `api`, `settings` | Workspace gorunum metinleri ve mobile consumption ayni kalir. |
## Sabit locale kullanimlari
Asagidaki kullanimlar Faz-2 format yardimcilariyla degistirilecek.
| Kullanim | Dosyalar |
| --- | --- |
| `<html lang="tr">` | `app/layout.tsx` |
| `date-fns/locale/tr` | `app/portal/page.tsx`, `app/portal/projects/page.tsx`, `app/portal/tasks/page.tsx`, `app/portal/revisions/page.tsx`, `app/portal/projects/[id]/portal-project-client.tsx`, `app/(dashboard)/clients/clients-client.tsx`, `app/(dashboard)/clients/[id]/client-detail-client.tsx`, `app/(dashboard)/business/proposals/proposals-client.tsx`, `app/(dashboard)/business/invoices/invoices-client.tsx`, `app/(dashboard)/business/subscriptions/subscriptions-client.tsx` |
| `Intl.*("tr-TR")` | `dashboard-client.tsx`, `calendar-client.tsx`, `finance-client.tsx`, `journal-client.tsx`, `projects-client.tsx`, `project-detail-client.tsx`, `tasks-client.tsx`, business client'lari |
| `toLocaleDateString("tr-TR")` | `dashboard-client.tsx`, `project-detail-client.tsx` |
| `localeCompare(..., "tr")` | `app/(dashboard)/projects/page.tsx` |
## Kullaniciya gorunen sabit metin kaynaklari
Turkce karakter iceren veya dogrudan kullaniciya donen string barindirma ihtimali
en yuksek alanlar:
| Klasor | Kapsam |
| --- | --- |
| `app/(dashboard)` | Freelancer ekranlarinin buyuk bolumu. |
| `app/portal` | Musteri portali. |
| `app/invite` | Auth oncesi portal daveti. |
| `app/login`, `app/register` | Auth ekranlari. |
| `app/api` | Kullaniciya donen JSON hata/basari metinleri. |
| `components/layout` | Sidebar, account menu ve shell metinleri. |
| `components/system` | Ortak page header, stat card ve empty/error state metinleri. |
| `config/sidebar.ts` | Freelancer navigasyon metinleri. |
| `config/portal-sidebar.ts` | Portal navigasyon metinleri. |
| `server/auth`, `server/services`, `server/ai` | Server action/API hata ve toast mesajlari. |
## Cevrilebilir domain alan registry'si
Ilk surumde asagidaki alanlar `content_translations` ile locale bazli
cevrilebilir kabul edilir.
| Entity | Field | Zorunlu locale | Not |
| --- | --- | --- | --- |
| `projects` | `name` | Instance default | Mevcut kolon backfill kaynagi. |
| `projects` | `description` | Opsiyonel | Portal ve freelancer detayinda kullanilir. |
| `projects` | `coverImageAlt` | Opsiyonel | A11y icin portalda onemli. |
| `tasks` | `title` | Instance default | Public/private fark etmeksizin ayni alan modeli. |
| `tasks` | `description` | Opsiyonel | Musteriye acik gorevlerde portalda gosterilir. |
| `calendar_events` | `title` | Instance default | Etkinlik ortak alandir, ileride portal visibility eklenirse hazir. |
| `calendar_events` | `description` | Opsiyonel | |
| `clients` | `notes` | Opsiyonel | Ilk surumde internal kalir; ceviri desteklenebilir ama portalda gosterilmez. |
| `planning_sections` | `title` | Instance default | Proje planlama bolumleri icin. |
| `planning_sections` | `content` | Opsiyonel | Rich text degilse plain text olarak saklanir. |
| `branding/settings` | `workspaceName` | Instance default | UI/portal basliklari ve mobile meta icin. |
| `branding/settings` | `metaTitle` | Instance default | Browser metadata. |
| `branding/settings` | `metaDescription` | Opsiyonel | SEO/public metadata. |
Ilk surumde cevrilmeyecek alanlar:
- Chat mesajlari.
- Revizyon talepleri.
- Gunluk notlari.
- Finans islem aciklamalari.
- Maliyet, tarih, para birimi, yuzde ve enum teknik degerleri.
## RTL test locale'i
Ilk release hedefi LTR diller olsa da layout kirilmasini erkenden gormek icin
test locale'i `ar-XB` olarak belirlendi.
Bu locale aktif urun dili olarak sunulmayacak. Faz-7'de `dir="rtl"` davranisini,
sidebar hizalamalarini, form tab'larini ve modal yerlesimlerini smoke etmek icin
fixture olarak kullanilacak.
@@ -0,0 +1,84 @@
---
title: Faz 0 Migration ve Geri Donus Sozlesmesi
description: Cok dillilik calismasi icin expand/migrate/contract sirasi ve backup kurallari.
phase: 0
status: completed
last_updated: 2026-07-19
---
# Faz 0 Migration ve Geri Donus Sozlesmesi
Bu sozlesme Faz-1'den itibaren veritabani degisikliklerinin nasil yapilacagini
tanımlar. Neta self-host bir urun oldugu icin migration'lar veri kaybi riski
tasimamali ve rollback hikayesi basit kalmalidir.
## Uygulama sirasi
1. Expand
Yeni tablolar ve yeni nullable kolonlar eklenir. Mevcut kolonlar kaldirilmaz,
renamelenmez ve zorunlu hale getirilmez.
2. Backfill
Mevcut Turkce veriler instance default locale'i kabul edilerek
`content_translations` icine kopyalanir. Orijinal kolonlar okunabilir kalir.
3. Dual read
Okuma katmani once translation resolver'a bakar, eksikse mevcut kolona duser.
Bu asamada eski veriler ve yeni veriler ayni anda calisir.
4. Dual write
Formlar instance default locale alanini hem mevcut kolona hem translation
tablosuna yazar. Ek locale'ler sadece translation tablosuna yazilir.
5. Contract
Ancak en az bir release sonra eski kolonlarin kaldirilmasi veya tamamen internal
fallback haline getirilmesi tartisilir. Ilk cok dillilik fazlarinda contract
adimi yapilmayacak.
## Backup adimlari
Her schema migration oncesi:
```bash
pnpm db:backup
```
Backup dosyasi deploy notuna yazilmalidir. Dokploy veya Docker deploy'da volume
path'i kontrol edilmeden migration calistirilmamalidir.
## Geri donus adimlari
Migration sonrasi kritik hata varsa:
1. Uygulama yeni surumden onceki image/commit'e geri alinir.
2. SQLite dosyasi backup'tan restore edilir.
3. `pnpm db:restore <backup-file>` veya host tarafindaki manuel restore adimi
kullanilir.
4. Restore sonrasi `pnpm phase9:smoke` ile health/API temel davranisi kontrol
edilir.
## Faz bazli DB dokunuslari
| Faz | DB degisikligi | Risk | Not |
| --- | --- | --- | --- |
| Faz-1 | Locale ve translation tablolari, `clients.portal_locale`, `portal_invitations.locale`, preference check constraint genisletme | Orta | Expand only. |
| Faz-2 | DB yok veya yalnizca katalog version seed'i | Dusuk | Runtime/cache agirlikli. |
| Faz-3 | UI translation CRUD | Orta | Admin-only mutation ve audit gerekli. |
| Faz-5 | Domain content translations dual write | Yuksek | Backfill ve resolver testleri sart. |
| Faz-6 | Portal locale davet akisi | Orta | Auth oncesi davet dili kritik. |
| Faz-8 | API contract genisletme | Dusuk/Orta | Backward compatible response alanlari eklenir. |
## Non-goal
Ilk cok dillilik release'inde asagidakiler yapilmayacak:
- Mevcut `projects.name`, `tasks.title` gibi kolonlari kaldirmak.
- Otomatik makine cevirisi eklemek.
- Public route'lara locale prefix eklemek.
- Chat/gunluk/revizyon taleplerini otomatik cevirmek.
- Kullanici yazili icerigini farkli locale'e sessizce overwrite etmek.
+76
View File
@@ -0,0 +1,76 @@
---
title: Faz 1 Locale Veri Modeli ve Service Katmani
description: Cok dillilik icin SQLite semasi, seed, locale yonetimi servisi ve smoke sonuclari.
phase: 1
status: completed
last_updated: 2026-07-19
---
# Faz 1 Locale Veri Modeli ve Service Katmani
Faz-1, cok dillilik icin runtime'a girmeden once gerekli kalici veri modelini
ve owner-only locale yonetimi kurallarini ekledi.
## Eklenen veri modeli
| Tablo veya kolon | Amac |
| --- | --- |
| `instance_locales` | Instance icindeki aktif, draft, archived ve test dillerini tutar. |
| `instance_i18n_settings` | Varsayilan locale ve katalog versiyonunu tutar. |
| `instance_ui_translations` | Built-in katalog ustune yazilacak instance UI cevirilerini tutar. |
| `content_translations` | Domain entity/field/locale bazli icerik cevirilerini tutar. |
| `clients.portal_locale` | Musteri portal dilini tutar. |
| `portal_invitations.locale` | Auth oncesi davet ekraninin dilini tutar. |
| `user_preferences.language` | Sabit `tr/en` constraint yerine 2-12 karakter locale kodu kabul eder. |
Migration `tr` ve `en` built-in locale'lerini seed eder, varsayilan instance
dilini `tr` olarak olusturur ve mevcut preference verisini kayipsiz tasir.
## Service kurallari
`I18nService` ile asagidaki kurallar kilitlendi:
- Locale yonetimi yalnizca freelancer/owner actor tarafindan yapilabilir.
- Disabled actor islem yapamaz.
- Locale kodu kisa BCP47 formatinda dogrulanir: `tr`, `en`, `fr`, `ar-XB`.
- `tr` ve `en` built-in, aktif ve arsivlenemez diller olarak korunur.
- Fallback locale mevcut ve arsivlenmemis olmak zorundadir.
- Locale kendi kendine fallback olamaz.
- Fallback zinciri dongu olusturamaz.
- Varsayilan dil yalnizca `active` locale olabilir.
- Default, fallback, user preference, client portal veya invitation tarafindan
kullanilan locale arsivlenemez.
## Verification
Calistirilan komutlar:
```bash
pnpm typecheck
pnpm i18n:phase1-smoke
pnpm build
pnpm phase9:smoke
```
Sonuclar basarili. Build sirasinda mevcut Next edge runtime statik uretim uyarisi
tekrar goruldu; Faz-1 ile ilgili yeni bir hata degil.
`i18n:phase1-smoke` su kapilari kontrol eder:
- Yeni migration tablolarini ve kolonlarini dogrular.
- `tr/en` seed ve default `tr` ayarini dogrular.
- `user_preferences.language` alaninin `fr` gibi yeni locale degeri kabul
ettigini dogrular.
- Owner/client negatif authorization testlerini calistirir.
- Invalid locale, duplicate built-in locale, self fallback ve fallback cycle
durumlarini reddeder.
- Draft locale'in default yapilamadigini dogrular.
- Referansli ve built-in locale'in arsivlenemedigini dogrular.
- Backup alip restore eder; `instance_i18n_settings` ve `content_translations`
verisinin restore sonrasinda korundugunu dogrular.
## Faz-2'ye kalan sinir
Bu faz henuz UI metinlerini cevirmedi ve runtime translator eklemedi. Faz-2'de
built-in kataloglar, locale resolver, formatter ve client/server translator
katmani bu veri modelini kullanacak.
+78
View File
@@ -0,0 +1,78 @@
---
title: Faz 2 Ceviri Runtime'i ve Built-in Kataloglar
description: Neta cok dillilik runtime, katalog, fallback, formatter, locale resolver ve smoke sonuclari.
phase: 2
status: completed
last_updated: 2026-07-19
---
# Faz 2 Ceviri Runtime'i ve Built-in Kataloglar
Faz-2, Faz-1'de eklenen locale veri modelini calisan runtime katmanina bagladi.
Bu faz tum sayfa metinlerini henuz migrate etmez; sayfa sayfa UI migration Faz-4
kapsaminda yapilacak.
## Eklenen runtime parcalari
| Alan | Dosya |
| --- | --- |
| I18n tipleri ve namespace listesi | `lib/i18n/types.ts` |
| Built-in katalog registry ve parity helper | `lib/i18n/catalog.ts` |
| Interpolation, plural, tarih, sayi ve para formatter | `lib/i18n/format.ts` |
| Client-safe translator | `lib/i18n/translator.ts` |
| React client provider ve hook'lar | `components/i18n/i18n-provider.tsx` |
| TR built-in katalog | `locales/tr/*` |
| EN built-in katalog | `locales/en/*` |
| Server catalog merge ve catalogVersion cache | `server/i18n/catalog.ts` |
| Locale cookie ve direction helper | `server/i18n/locale.ts` |
| Session/cookie/instance locale resolver | `server/i18n/resolver.ts` |
| Server `createTranslator` helper | `server/i18n/translator.ts` |
| Public localization metadata | `server/i18n/runtime.ts` |
| Locale cookie lifecycle route | `app/api/i18n/locale/route.ts` |
## Davranis
- Built-in `tr` ve `en` kataloglari ayni namespace/key setine sahiptir.
- Ceviri anahtarlari `namespace.key` formatinda kullanilir.
- Interpolation `{name}` formatini destekler.
- Basit plural formatinda `Intl.PluralRules` kullanilir.
- DB override, built-in katalog ustune merge edilir.
- Fallback sirasi locale kaydindaki fallback chain'e gore calisir.
- Eksik anahtar production'da ham key'e duser; development'ta console warning
verir.
- `catalog_version` override mutation'inda artar ve cache key'ine dahil edilir.
- `<html lang>` ve `<html dir>` request locale resolver'dan gelir.
- `neta_locale` cookie'si `/api/i18n/locale` route'u uzerinden yazilir.
- `/api/v1/meta` response'u localization metadata'si dondurur.
## Verification
Calistirilan komutlar:
```bash
pnpm i18n:phase2-smoke
pnpm build
pnpm phase9:smoke
pnpm typecheck
```
Sonuclar basarili. `i18n:phase2-smoke` su kapilari dogrular:
- TR/EN katalog parity.
- Ayni translation helper ile TR ve EN render.
- Interpolation ve plural format.
- Ortak tarih ve para formatter.
- RTL direction helper.
- Fransizca eksik anahtarda EN fallback.
- DB UI translation override'in deployment olmadan okunmasi.
- Override sonrasi `catalog_version` bump ile yeni metnin okunmasi.
- Owner disi actor'un UI translation yazamamasi.
`pnpm build` sirasinda mevcut Next edge runtime static generation uyarisi tekrar
goruldu; Faz-2 kaynakli yeni bir hata degil.
## Faz-4'e kalan sinir
Bu fazda mevcut sayfa metinleri henuz topluca katalog anahtarlarina tasinmadi.
Dashboard, musteri, proje, gorev, finans, gunluk, chat ve portal ekranlarindaki
hardcoded metinlerin migration'i Faz-4 ve Faz-6 kapsaminda yapilacak.
+59
View File
@@ -0,0 +1,59 @@
---
title: Faz 3 Ayarlar Dil ve Ceviri Yonetimi
description: Ayarlar ekraninda locale yonetimi, UI translation editor, import/export ve dogrulama sonuclari.
phase: 3
status: completed
last_updated: 2026-07-19
---
# Faz 3 Ayarlar Dil ve Ceviri Yonetimi
Faz-3, Faz-1 ve Faz-2 i18n altyapisini owner tarafindan yonetilebilir hale
getirdi.
## Eklenenler
- Ayarlar sayfasina `Diller ve ceviriler` tab'i eklendi.
- Owner yeni locale ekleyebilir.
- Locale `draft`, `active`, `archived` lifecycle'i UI uzerinden yonetilebilir.
- Aktif locale instance default dili yapilabilir.
- Freelancer kendi arayuz dilini secebilir; tercih `user_preferences.language`
ve `neta_locale` cookie'sine yazilir.
- Locale kartlarinda genel tamamlanma yuzdesi ve eksik anahtar sayisi gorunur.
- Namespace ve metin aramali UI translation editor eklendi.
- Editor Turkce ve Ingilizce referans metnini ayni satirda gosterir.
- Tekil translation override kaydedilebilir ve sifirlanabilir.
- JSON export paketi uretilebilir.
- JSON import preview ve commit akisi eklendi.
- I18n mutation'lari `runtime_events` icine `i18n.settings` olarak audit edilir.
- Import/export round-trip ve client negatif yetki smoke testi eklendi.
## Degisen dosyalar
| Alan | Dosya |
| --- | --- |
| Settings UI | `app/(dashboard)/settings/page.tsx` |
| Settings server actions | `app/(dashboard)/settings/actions.ts` |
| I18n service export/import/completion | `server/i18n/service.ts` |
| UI translation repository helpers | `server/repositories/i18n.ts` |
| User language preference service | `server/settings/preferences.ts` |
| Faz-3 smoke | `scripts/i18n-phase3-smoke.*` |
## Verification
Calistirilan komutlar:
```bash
pnpm typecheck
pnpm i18n:phase3-smoke
pnpm build
pnpm phase9:smoke
```
Sonuclar basarili. `pnpm build` sirasinda mevcut Next edge runtime static
generation uyarisi tekrar goruldu; Faz-3 kaynakli yeni bir hata degil.
## Faz-4'e kalan sinir
Bu faz ceviri yonetimini ekledi; uygulamadaki tum hardcoded metinleri henuz
katalog anahtarina tasimadi. Sayfa sayfa metin migration'i Faz-4'te yapilacak.
@@ -0,0 +1,265 @@
---
title: Faz 4 Kalan Hardcoded Metin Raporu
phase: 4
status: generated
last_updated: 2026-07-18T23:56:57.608Z
---
# Faz 4 Kalan Hardcoded Metin Raporu
Bu rapor Faz-4 boundary script'i tarafindan uretilir. Formatter sabitleri release blocker kabul edilir; kalan Turkce stringler Faz-4 kapsaminda raporlanir ve sonraki UI migration dalgalarinda eritilir.
Toplam kalan Turkce literal satiri: 294
- `app/(dashboard)/analytics/analytics-client.tsx:56` <SelectValue placeholder="Tarih aralığı" />
- `app/(dashboard)/business/subscriptions/subscriptions-client.tsx:38` return "Aylık";
- `app/(dashboard)/business/subscriptions/subscriptions-client.tsx:40` return "Yıllık";
- `app/(dashboard)/business/subscriptions/subscriptions-client.tsx:42` return "Haftalık";
- `app/(dashboard)/calendar/actions.ts:17` title: requiredText(formData.get("title"), "Etkinlik başlığı zorunludur."),
- `app/(dashboard)/calendar/actions.ts:46` if (!value.startsAt) throw new Error("Etkinlik başlangıç zamanı zorunludur.");
- `app/(dashboard)/calendar/actions.ts:53` const id = requiredText(formData.get("id"), "Etkinlik kaydı bulunamadı.");
- `app/(dashboard)/calendar/actions.ts:55` if (!value.startsAt) throw new Error("Etkinlik başlangıç zamanı zorunludur.");
- `app/(dashboard)/calendar/actions.ts:64` requiredText(formData.get("id"), "Silinecek etkinlik bulunamadı."),
- `app/(dashboard)/calendar/calendar-client.tsx:55` meeting: "Toplantı",
- `app/(dashboard)/calendar/calendar-client.tsx:58` personal: "Kişisel",
- `app/(dashboard)/calendar/calendar-client.tsx:134` {["Pzt", "Sal", "Çar", "Per", "Cum", "Cmt", "Paz"].map((day) => (
- `app/(dashboard)/calendar/calendar-client.tsx:245` {event.projectName || event.clientName || event.taskTitle || event.description || "Bağlantı yok"}
- `app/(dashboard)/calendar/calendar-client.tsx:293` toast.success(mode === "create" ? "Etkinlik eklendi." : "Etkinlik güncellendi.");
- `app/(dashboard)/calendar/calendar-client.tsx:298` : "Etkinlik kaydedilirken beklenmeyen bir hata oluştu.",
- `app/(dashboard)/calendar/calendar-client.tsx:310` {mode === "create" ? "Etkinlik ekle" : "Düzenle"}
- `app/(dashboard)/calendar/calendar-client.tsx:317` <DialogTitle>{mode === "create" ? "Yeni etkinlik" : "Etkinliği düzenle"}</DialogTitle>
- `app/(dashboard)/calendar/calendar-client.tsx:328` {isSubmitting ? "Kaydediliyor" : mode === "create" ? "Etkinliği ekle" : "Değişiklikleri kaydet"}
- `app/(dashboard)/calendar/calendar-client.tsx:356` <Input name="title" defaultValue={event?.title || ""} required placeholder="Örn. Müşteri toplantısı" />
- `app/(dashboard)/calendar/calendar-client.tsx:363` <SelectField name="type" label="Tür" defaultValue={event?.type || "focus"}>
- `app/(dashboard)/calendar/calendar-client.tsx:370` <SelectField name="client_id" label="Müşteri" defaultValue={event?.client_id || "__none"}>
- `app/(dashboard)/calendar/calendar-client.tsx:380` <SelectField name="task_id" label="Görev" defaultValue={event?.task_id || "__none"}>
- `app/(dashboard)/calendar/calendar-client.tsx:404` <SelectTrigger><SelectValue placeholder={`${label} seç`} /></SelectTrigger>
- `app/(dashboard)/chat/page.tsx:58` toast.error(error.message || "Yapay zeka ile iletişim kurulurken bir hata oluştu.");
- `app/(dashboard)/chat/page.tsx:74` toast.error(error instanceof Error ? error.message : "Sohbetler yüklenemedi.");
- `app/(dashboard)/chat/page.tsx:97` toast.error(error instanceof Error ? error.message : "Mesajlar yüklenemedi.");
- `app/(dashboard)/chat/page.tsx:143` toast.error(error instanceof Error ? error.message : "Sohbet oluşturulamadı.");
- `app/(dashboard)/chat/page.tsx:189` {session.title || "İsimsiz sohbet"}
- `app/(dashboard)/chat/page.tsx:196` aria-label={`${session.title || "İsimsiz sohbet"} sohbetini sil`}
- `app/(dashboard)/chat/page.tsx:302` placeholder="Mesaj gönder..."
- `app/(dashboard)/clients/[id]/actions.ts:19` title: requiredText(formData.get("title"), "Aktivite başlığı zorunludur."),
- `app/(dashboard)/clients/[id]/client-detail-client.tsx:95` throw new Error(data.error || "Kullanıcı oluşturulamadı.");
- `app/(dashboard)/clients/[id]/client-detail-client.tsx:99` toast.success("Güvenli portal daveti oluşturuldu.");
- `app/(dashboard)/clients/[id]/client-detail-client.tsx:101` toast.error(error instanceof Error ? error.message : "Davet oluşturulamadı.");
- `app/(dashboard)/clients/[id]/client-detail-client.tsx:116` if (!response.ok || data.error) throw new Error(data.error || "Portal dili güncellenemedi.");
- `app/(dashboard)/clients/[id]/client-detail-client.tsx:117` toast.success("Portal dili güncellendi.");
- `app/(dashboard)/clients/[id]/client-detail-client.tsx:120` toast.error(error instanceof Error ? error.message : "Portal dili güncellenemedi.");
- `app/(dashboard)/clients/[id]/client-detail-client.tsx:165` <SelectValue placeholder="Dil seç" />
- `app/(dashboard)/clients/[id]/client-detail-client.tsx:185` aria-label="Davet bağlantısını kopyala"
- `app/(dashboard)/clients/[id]/client-detail-client.tsx:188` toast.success("Davet bağlantısı kopyalandı.");
- `app/(dashboard)/clients/[id]/client-detail-client.tsx:310` <Input name="title" required placeholder="Aktivite özeti" />
- `app/(dashboard)/clients/[id]/client-detail-client.tsx:318` <Textarea name="content" rows={4} placeholder="Görüşme detayları..." />
- `app/(dashboard)/clients/actions.ts:25` name: requiredText(formData.get("name"), "Müşteri adı zorunludur."),
- `app/(dashboard)/clients/actions.ts:45` const id = requiredText(formData.get("id"), "Müşteri kaydı bulunamadı.");
- `app/(dashboard)/clients/actions.ts:53` const id = requiredText(formData.get("id"), "Arşivlenecek müşteri bulunamadı.");
- `app/(dashboard)/clients/clients-client.tsx:76` { id: "contacted", label: "İletişime Geçildi", color: "border-blue-200 bg-blue-50 text-blue-700" },
- `app/(dashboard)/clients/clients-client.tsx:77` { id: "proposal_sent", label: "Teklif İletildi", color: "border-amber-200 bg-amber-50 text-amber-700" },
- `app/(dashboard)/clients/clients-client.tsx:78` { id: "won", label: "Kazanıldı (Won)", color: "border-emerald-200 bg-emerald-50 text-emerald-700" },
- `app/(dashboard)/clients/clients-client.tsx:126` toast.success("Müşteri aşaması güncellendi.");
- `app/(dashboard)/clients/clients-client.tsx:135` : "Müşteri aşaması güncellenemedi.",
- `app/(dashboard)/clients/clients-client.tsx:175` label="Aktif Müşteri"
- `app/(dashboard)/clients/clients-client.tsx:187` label="Kayıtlı Gelir"
- `app/(dashboard)/clients/clients-client.tsx:189` description="Ödenmiş gelir işlemleri"
- `app/(dashboard)/clients/clients-client.tsx:205` placeholder="Müşteri, firma, e-posta veya not ara"
- `app/(dashboard)/clients/clients-client.tsx:433` toast.success(mode === "create" ? "Müşteri eklendi." : "Müşteri güncellendi.");
- `app/(dashboard)/clients/clients-client.tsx:438` : "Müşteri kaydedilirken beklenmeyen bir hata oluştu.",
- `app/(dashboard)/clients/clients-client.tsx:454` {mode === "create" ? "Müşteri ekle" : "Düzenle"}
- `app/(dashboard)/clients/clients-client.tsx:463` {mode === "create" ? "Yeni müşteri" : "Müşteriyi düzenle"}
- `app/(dashboard)/clients/clients-client.tsx:480` ? "Müşteriyi ekle"
- `app/(dashboard)/clients/clients-client.tsx:481` : "Değişiklikleri kaydet"}
- `app/(dashboard)/clients/clients-client.tsx:501` placeholder="Örn. Acme Corp"
- `app/(dashboard)/clients/clients-client.tsx:520` <SelectValue placeholder="Aşama seç" />
- `app/(dashboard)/clients/clients-client.tsx:533` <SelectValue placeholder="Durum seç" />
- `app/(dashboard)/clients/clients-client.tsx:583` placeholder="İletişim notları, beklentiler, özel bilgiler..."
- `app/(dashboard)/clients/clients-client.tsx:610` {hasQuery ? "Aramana uygun müşteri yok" : "Henüz müşteri eklenmedi"}
- `app/(dashboard)/dashboard-client.tsx:250` {project.status === 'completed' ? 'Tamamlandı' : project.status === 'active' ? 'Aktif' : 'Beklemede'}
- `app/(dashboard)/finance/actions.ts:52` const id = requiredText(formData.get("id"), "Finans kaydı bulunamadı.");
- `app/(dashboard)/finance/actions.ts:67` requiredText(formData.get("id"), "Silinecek finans kaydı bulunamadı."),
- `app/(dashboard)/finance/finance-client.tsx:68` planned: "Planlandı",
- `app/(dashboard)/finance/finance-client.tsx:70` paid: "Ödendi",
- `app/(dashboard)/finance/finance-client.tsx:71` cancelled: "İptal edildi",
- `app/(dashboard)/finance/finance-client.tsx:84` { value: "TRY", label: "Türk lirası (TRY)" },
- `app/(dashboard)/finance/finance-client.tsx:86` { value: "CAD", label: "Kanada doları (CAD)" },
- `app/(dashboard)/finance/finance-client.tsx:87` { value: "AUD", label: "Avustralya doları (AUD)" },
- `app/(dashboard)/finance/finance-client.tsx:93` { key: "afterTax", label: "Vergi Sonrası Net", tone: "green", icon: Wallet, featured: true },
- `app/(dashboard)/finance/finance-client.tsx:94` { key: "net", label: "Brüt kazanç", tone: "primary", icon: Wallet, featured: true },
- `app/(dashboard)/finance/finance-client.tsx:95` { key: "income", label: "Aylık gelir", tone: "green", icon: ArrowUpRight, featured: false },
- `app/(dashboard)/finance/finance-client.tsx:96` { key: "expense", label: "Aylık gider", tone: "rose", icon: ArrowDownRight, featured: false },
- `app/(dashboard)/finance/finance-client.tsx:177` aria-label="Önceki finans özet kartları"
- `app/(dashboard)/finance/finance-client.tsx:187` aria-label="Sonraki finans özet kartları"
- `app/(dashboard)/finance/finance-client.tsx:198` aria-label="Kaydırılabilir finans özeti"
- `app/(dashboard)/finance/finance-client.tsx:240` placeholder="Kategori, müşteri, proje veya açıklama ara"
- `app/(dashboard)/finance/finance-client.tsx:335` {transaction.category || "Kategori yok"} · {transaction.projectName || transaction.clientName || "Bağlantı yok"}
- `app/(dashboard)/finance/finance-client.tsx:383` toast.success(mode === "create" ? "İşlem eklendi." : "İşlem güncellendi.");
- `app/(dashboard)/finance/finance-client.tsx:388` : "Finans işlemi kaydedilirken beklenmeyen bir hata oluştu.",
- `app/(dashboard)/finance/finance-client.tsx:400` {mode === "create" ? "İşlem ekle" : "Düzenle"}
- `app/(dashboard)/finance/finance-client.tsx:407` <DialogTitle>{mode === "create" ? "Yeni finans işlemi" : "Finans işlemini düzenle"}</DialogTitle>
- `app/(dashboard)/finance/finance-client.tsx:416` {isSubmitting ? "Kaydediliyor" : mode === "create" ? "İşlemi ekle" : "Değişiklikleri kaydet"}
- `app/(dashboard)/finance/finance-client.tsx:476` <SelectField name="payment_status" label="Ödeme durumu" defaultValue={transaction?.payment_status || "planned"}>
- `app/(dashboard)/finance/finance-client.tsx:492` <SelectValue placeholder="Para birimi seç" />
- `app/(dashboard)/finance/finance-client.tsx:513` <Input name="category" defaultValue={transaction?.category || ""} placeholder="Örn. Yazılım, müşteri ödemesi, vergi" />
- `app/(dashboard)/finance/finance-client.tsx:526` <SelectValue placeholder="Müşteri seç" />
- `app/(dashboard)/finance/finance-client.tsx:542` <SelectValue placeholder="Proje seç" />
- `app/(dashboard)/finance/finance-client.tsx:568` <SelectTrigger><SelectValue placeholder={`${label} seç`} /></SelectTrigger>
- `app/(dashboard)/finance/finance-client.tsx:580` {hasQuery ? "Aramana uygun işlem yok" : "Henüz finans işlemi eklenmedi"}
- `app/(dashboard)/finance/finance-client.tsx:584` ? "Arama metnini sadeleştirerek tekrar deneyebilirsin."
- `app/(dashboard)/finance/finance-client.tsx:585` : "İlk gelir veya gider kaydını ekleyerek aylık finans özetini oluşturmaya başlayabilirsin."}
- `app/(dashboard)/finance/finance-client.tsx:647` throw new Error(data.error || "Bilinmeyen bir hata oluştu.");
- `app/(dashboard)/finance/finance-client.tsx:652` `Hata: ${error instanceof Error ? error.message : "Bilinmeyen bir hata oluştu."}`,
- `app/(dashboard)/journal/actions.ts:15` if (!moodScore || !energyScore) throw new Error("Mood ve enerji skorları zorunludur.");
- `app/(dashboard)/journal/actions.ts:35` requiredText(formData.get("id"), "Günlük kaydı bulunamadı."),
- `app/(dashboard)/journal/actions.ts:45` requiredText(formData.get("id"), "Silinecek günlük kaydı bulunamadı."),
- `app/(dashboard)/journal/journal-client.tsx:57` 1: "Çok düşük",
- `app/(dashboard)/journal/journal-client.tsx:58` 2: "Düşük",
- `app/(dashboard)/journal/journal-client.tsx:60` 4: "İyi",
- `app/(dashboard)/journal/journal-client.tsx:61` 5: "Çok iyi",
- `app/(dashboard)/journal/journal-client.tsx:114` label="Kayıtlı gün"
- `app/(dashboard)/journal/journal-client.tsx:257` toast.success(mode === "create" ? "Günlük eklendi." : "Günlük güncellendi.");
- `app/(dashboard)/journal/journal-client.tsx:262` : "Günlük kaydedilirken beklenmeyen bir hata oluştu.",
- `app/(dashboard)/journal/journal-client.tsx:274` {mode === "create" ? "Günlük ekle" : "Düzenle"}
- `app/(dashboard)/journal/journal-client.tsx:281` <DialogTitle>{mode === "create" ? "Yeni günlük kayıt" : "Günlük kaydı düzenle"}</DialogTitle>
- `app/(dashboard)/journal/journal-client.tsx:293` {isSubmitting ? "Kaydediliyor" : mode === "create" ? "Kaydı ekle" : "Değişiklikleri kaydet"}
- `app/(dashboard)/journal/journal-client.tsx:332` label="Çalışma memnuniyeti"
- `app/(dashboard)/journal/journal-client.tsx:343` placeholder="Bugün nasıl geçti, enerjini etkileyen şeyler nelerdi?"
- `app/(dashboard)/journal/journal-client.tsx:418` insights.push("Henüz okunabilir bir trend yok.");
- `app/(dashboard)/journal/journal-client.tsx:420` insights.push(`Toplam ${logs.length} günlük kayıt var.`);
- `app/(dashboard)/journal/journal-client.tsx:423` ? "Enerji ortalaması düşük. Dashboard raporlarında geciken işler ile birlikte okunmalı."
- `app/(dashboard)/journal/journal-client.tsx:424` : "Enerji ortalaması dengeli görünüyor.",
- `app/(dashboard)/journal/journal-client.tsx:428` ? "Mood seviyesi güçlü. Yüksek odak isteyen işler için iyi bir dönem olabilir."
- `app/(dashboard)/journal/journal-client.tsx:429` : "Mood trendi izlenmeli. Not alanı hangi günlerin zor geçtiğini anlamak için önemli.",
- `app/(dashboard)/layout.tsx:35` const displayName = profile.displayName || user.name || user.email.split("@")[0] || "Neta Kullanıcısı";
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:136` client_project: "Müşteri projesi",
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:143` paused: "Duraklatıldı",
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:144` completed: "Tamamlandı",
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:145` cancelled: "İptal edildi",
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:164` overview: "Genel bakış",
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:165` problem: "Çözdüğü problem",
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:166` goal: "Amaç",
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:172` assets: "Görsel varlıklar",
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:249` pendingChildren="Tamamlanıyor"
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:278` <InfoItem label="Tür" value={typeLabels[project.type]} icon={FolderKanban} />
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:280` label="Müşteri"
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:281` value={project.clientName || "Bağımsız side project"}
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:290` label="Bütçe"
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:294` : "Bütçe yok"
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:303` <StatCard label="İlerleme" value={`${project.progress}%`} icon={Target} />
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:304` <StatCard label="Görev" value={`${doneTaskCount}/${tasks.length}`} icon={ClipboardList} />
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:339` title="Planlama alanları"
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:340` description="Problem, amaç, hedef kitle, kapsam ve proje notlarını burada tut."
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:351` description="Renk paleti, tipografi, görsel dil ve asset notlarını proje kaynağına bağla."
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:388` : "Revizyon durumu güncellenemedi.",
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:517` {section.content || "İçerik eklenmedi."}
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:572` {mode === "create" ? "Planlama alanı ekle" : "Planlama alanını düzenle"}
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:584` <SelectValue placeholder="Kategori seç" />
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:668` : "Görev durumu güncellenemedi.",
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:757` ? "Tamamlandı"
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:760` : "Yapılacak"}
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:784` {pendingTaskIds.has(task.id) ? "Tamamlanıyor" : "Tamamla"}
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:803` <EmptyPanel icon={ClipboardList} title="Bu projeye bağlı görev yok" />
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:1059` <SelectValue placeholder="Durum seç" />
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:1072` <SelectValue placeholder="Öncelik seç" />
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:1135` {isSubmitting ? "Kaydediliyor" : "Görevi ekle"}
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:1181` <EmptyPanel icon={Wallet} title="Bu projeye bağlı finans kaydı yok" />
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:1288` if (status === "done") return "Tamamlandı";
- `app/(dashboard)/projects/[id]/project-detail-client.tsx:1290` return "Yapılacak";
- `app/(dashboard)/projects/actions.ts:32` name: localized.name ?? requiredText(formData.get("name"), "Proje adı zorunludur."),
- `app/(dashboard)/projects/actions.ts:86` const id = requiredText(formData.get("id"), "Proje kaydı bulunamadı.");
- `app/(dashboard)/projects/actions.ts:96` const id = requiredText(formData.get("id"), "Tamamlanacak proje bulunamadı.");
- `app/(dashboard)/projects/actions.ts:107` title: localized.title ?? requiredText(formData.get("title"), "Planlama başlığı zorunludur."),
- `app/(dashboard)/projects/actions.ts:129` const id = requiredText(formData.get("id"), "Planlama alanı bulunamadı.");
- `app/(dashboard)/projects/actions.ts:132` throw new Error("Planlama alanı bu projeye ait değil.");
- `app/(dashboard)/projects/actions.ts:147` const id = requiredText(formData.get("id"), "Silinecek planlama alanı bulunamadı.");
- `app/(dashboard)/projects/actions.ts:150` throw new Error("Planlama alanı bu projeye ait değil.");
- `app/(dashboard)/projects/projects-client.tsx:77` client_project: "Müşteri projesi",
- `app/(dashboard)/projects/projects-client.tsx:84` paused: "Duraklatıldı",
- `app/(dashboard)/projects/projects-client.tsx:85` completed: "Tamamlandı",
- `app/(dashboard)/projects/projects-client.tsx:86` cancelled: "İptal edildi",
- `app/(dashboard)/projects/projects-client.tsx:161` placeholder="Proje, müşteri veya açıklama ara"
- `app/(dashboard)/projects/projects-client.tsx:248` aria-label={`${project.name} detayına git`}
- `app/(dashboard)/projects/projects-client.tsx:276` {project.description || "Açıklama eklenmedi."}
- `app/(dashboard)/projects/projects-client.tsx:333` {project.clientName || "Bağımsız side project"}
- `app/(dashboard)/projects/projects-client.tsx:354` <div>{project.clientName || "Müşteri bağlantısı yok"}</div>
- `app/(dashboard)/projects/projects-client.tsx:359` <div>{project.budget_amount ? formatCurrency(project.budget_amount) : "Bütçe yok"}</div>
- `app/(dashboard)/projects/projects-client.tsx:437` toast.success(mode === "create" ? "Proje eklendi." : "Proje güncellendi.");
- `app/(dashboard)/projects/projects-client.tsx:442` : "Proje kaydedilirken beklenmeyen bir hata oluştu.",
- `app/(dashboard)/projects/projects-client.tsx:456` title={mode === "create" ? "Proje ekle" : "Düzenle"}
- `app/(dashboard)/projects/projects-client.tsx:457` aria-label={mode === "create" ? "Proje ekle" : "Düzenle"}
- `app/(dashboard)/projects/projects-client.tsx:460` {iconOnly ? null : mode === "create" ? "Proje ekle" : "Düzenle"}
- `app/(dashboard)/projects/projects-client.tsx:467` <DialogTitle>{mode === "create" ? "Yeni proje" : "Projeyi düzenle"}</DialogTitle>
- `app/(dashboard)/projects/projects-client.tsx:490` : "Değişiklikleri kaydet"}
- `app/(dashboard)/projects/projects-client.tsx:539` alt={project?.cover_image_alt || project?.name || "Proje kapak görseli önizlemesi"}
- `app/(dashboard)/projects/projects-client.tsx:614` <SelectValue placeholder="Tür seç" />
- `app/(dashboard)/projects/projects-client.tsx:630` <SelectValue placeholder="Müşteri seç" />
- `app/(dashboard)/projects/projects-client.tsx:648` <SelectValue placeholder="Durum seç" />
- `app/(dashboard)/projects/projects-client.tsx:736` {hasQuery ? "Aramana uygun proje yok" : "Henüz proje eklenmedi"}
- `app/(dashboard)/projects/projects-client.tsx:740` ? "Arama metnini sadeleştirerek tekrar deneyebilirsin."
- `app/(dashboard)/projects/projects-client.tsx:741` : "İlk müşteri projen veya side project kaydınla operasyon akışını kurmaya başlayabilirsin."}
- `app/(dashboard)/projects/projects-client.tsx:779` throw new Error(data.error || "Bilinmeyen bir hata oluştu.");
- `app/(dashboard)/projects/projects-client.tsx:785` (err instanceof Error ? err.message : "Bilinmeyen bir hata oluştu."),
- `app/(dashboard)/settings/actions.ts:113` return { error: error instanceof Error ? error.message : "Profil güncellenemedi." };
- `app/(dashboard)/settings/actions.ts:122` return { error: "Mevcut şifre zorunludur; yeni şifre en az 8 karakter olmalıdır." };
- `app/(dashboard)/settings/actions.ts:137` return { error: "Mevcut şifre doğrulanamadı veya şifre güncellenemedi." };
- `app/(dashboard)/settings/actions.ts:216` return { error: error instanceof Error ? error.message : "Dil durumu güncellenemedi." };
- `app/(dashboard)/settings/actions.ts:229` return { error: error instanceof Error ? error.message : "Varsayılan dil güncellenemedi." };
- `app/(dashboard)/settings/actions.ts:247` return { error: error instanceof Error ? error.message : "Çeviri kaydedilemedi." };
- `app/(dashboard)/settings/actions.ts:264` return { error: error instanceof Error ? error.message : "Çeviri sıfırlanamadı." };
- `app/(dashboard)/settings/actions.ts:275` return { error: error instanceof Error ? error.message : "Çeviri paketi dışa aktarılamadı." };
- `app/(dashboard)/settings/actions.ts:290` return { error: error instanceof Error ? error.message : "Import paketi okunamadı." };
- `app/(dashboard)/settings/actions.ts:303` return { error: error instanceof Error ? error.message : "Çeviri paketi içe aktarılamadı." };
- `app/(dashboard)/settings/actions.ts:321` return { error: "Workspace adı 1-120 karakter arasında olmalıdır." };
- `app/(dashboard)/settings/actions.ts:324` return { error: "Tarayıcı başlığı 1-80 karakter arasında olmalıdır." };
- `app/(dashboard)/settings/actions.ts:327` return { error: "Kısa uygulama adı 1-24 karakter arasında olmalıdır." };
- `app/(dashboard)/settings/actions.ts:330` return { error: "Ana renk #RRGGBB formatında olmalıdır." };
- `app/(dashboard)/settings/actions.ts:394` return { error: "Geçersiz marka görseli." };
- `app/(dashboard)/settings/actions.ts:410` return { error: error instanceof Error ? error.message : "Marka görseli kaldırılamadı." };
- `app/(dashboard)/settings/page.tsx:91` label: "Açık",
- `app/(dashboard)/settings/page.tsx:92` description: "Her zaman aydınlık renk paletini kullanır.",
- `app/(dashboard)/settings/page.tsx:98` description: "Her zaman koyu renk paletini kullanır.",
- `app/(dashboard)/settings/page.tsx:104` description: "Cihazınızın görünüm tercihini otomatik takip eder.",
- `app/(dashboard)/settings/page.tsx:162` nativeName: "Français",
- `app/(dashboard)/settings/page.tsx:176` { name: "Diller ve çeviriler", icon: Languages },
- `app/(dashboard)/settings/page.tsx:237` toast.success("Profil güncellendi!");
- `app/(dashboard)/settings/page.tsx:248` toast.success("Şifre güncellendi!");
- `app/(dashboard)/settings/page.tsx:261` toast.success("Yapay Zeka ayarları kaydedildi!");
- `app/(dashboard)/settings/page.tsx:281` toast.success("Görünüm tercihi kaydedildi.");
- `app/(dashboard)/settings/page.tsx:308` toast.success("Genel görünüm ve marka ayarları güncellendi.");
- `app/(dashboard)/settings/page.tsx:324` toast.success("Marka görseli kaldırıldı.");
- `app/(dashboard)/settings/page.tsx:349` toast.success("Arayüz dili kaydedildi.");
- `app/(dashboard)/settings/page.tsx:370` toast.success("Dil durumu güncellendi.");
- `app/(dashboard)/settings/page.tsx:380` toast.success("Varsayılan dil güncellendi.");
- `app/(dashboard)/settings/page.tsx:396` toast.success("Çeviri kaydedildi.");
- `app/(dashboard)/settings/page.tsx:410` toast.success("Override sıfırlandı.");
- `app/(dashboard)/settings/page.tsx:418` toast.error(response.error ?? "Export oluşturulamadı.");
- `app/(dashboard)/settings/page.tsx:422` toast.success("Çeviri paketi aşağıdaki alana yazıldı.");
- `app/(dashboard)/settings/page.tsx:429` toast.error(response.error ?? "Import paketi okunamadı.");
- `app/(dashboard)/settings/page.tsx:432` setImportPreview(`${response.preview.localeCount} dil, ${response.preview.translationCount} çeviri, default: ${response.preview.defaultLocale}`);
- `app/(dashboard)/settings/page.tsx:441` toast.success("Çeviri paketi içe aktarıldı.");
- `app/(dashboard)/settings/page.tsx:594` description="Kare PNG önerilir; en fazla 5 MB."
- `app/(dashboard)/settings/page.tsx:620` aria-label="Ana renk seçici"
- `app/(dashboard)/settings/page.tsx:678` aria-label="Tema görünümü"
- `app/(dashboard)/settings/page.tsx:726` ? "Görünüm tercihi kaydediliyor…"
- `app/(dashboard)/settings/page.tsx:727` : "Değişiklik tüm sayfalara anında uygulanır."}
- `app/(dashboard)/settings/page.tsx:734` {activeTab === "Diller ve çeviriler" && (
- `app/(dashboard)/settings/page.tsx:795` {defaultLocale === locale.code ? "Varsayılan" : "Default yap"}
- `app/(dashboard)/settings/page.tsx:815` <Input value={newLocale.nativeName} onChange={(event) => setNewLocale((current) => ({ ...current, nativeName: event.target.value }))} placeholder="Français" />
- `app/(dashboard)/settings/page.tsx:1065` placeholder={hasApiKey ? "Kayıtlı anahtarı korumak için boş bırakın" : "sk-..."}
- `app/(dashboard)/settings/page.tsx:1171` alt={`${title} önizlemesi`}
- `app/(dashboard)/tasks/actions.ts:28` title: localized.title ?? requiredText(formData.get("title"), "Görev başlığı zorunludur."),
- `app/(dashboard)/tasks/actions.ts:72` const id = requiredText(formData.get("id"), "Görev kaydı bulunamadı.");
- `app/(dashboard)/tasks/actions.ts:81` const id = requiredText(formData.get("id"), "Tamamlanacak görev bulunamadı.");
- `app/(dashboard)/tasks/actions.ts:95` const id = requiredText(formData.get("id"), "Silinecek görev bulunamadı.");
- `app/(dashboard)/tasks/tasks-client.tsx:64` todo: "Yapılacak",
- `app/(dashboard)/tasks/tasks-client.tsx:66` done: "Tamamlandı",
- `app/(dashboard)/tasks/tasks-client.tsx:70` low: "Düşük",
- `app/(dashboard)/tasks/tasks-client.tsx:72` high: "Yüksek",
- `app/(dashboard)/tasks/tasks-client.tsx:129` : "Görev durumu güncellenemedi.",
- `app/(dashboard)/tasks/tasks-client.tsx:159` error instanceof Error ? error.message : "Görev silinemedi.",
- `app/(dashboard)/tasks/tasks-client.tsx:214` <StatCard label="Toplam görev" value={localTasks.length.toString()} />
- `app/(dashboard)/tasks/tasks-client.tsx:233` placeholder="Görev, proje veya müşteri ara"
- `app/(dashboard)/tasks/tasks-client.tsx:379` <div>{task.clientName || "Müşteri yok"}</div>
Ilk 250 satir listelendi; kalan: 44
+54
View File
@@ -0,0 +1,54 @@
---
title: Faz 4 Freelancer UI Migration
description: Freelancer uygulamasinda ortak shell, sidebar, dashboard ve locale-aware formatter migration sonuclari.
phase: 4
status: completed
last_updated: 2026-07-19
---
# Faz 4 Freelancer UI Migration
Faz-4, owner/freelancer uygulamasinda i18n runtime'in gercek UI yuzeylerine
baglanmasini baslatti ve teknik formatter sabitlerini kaldirdi.
## Tamamlananlar
- Dashboard layout request locale'e baglandi.
- Dashboard subtree `I18nProvider` ile sarildi.
- Sidebar grup ve linkleri katalog anahtarlarindan uretiliyor.
- App shell skip link, mobil menu, hesap dropdown'i, cikis ve progress metinleri
locale-aware hale geldi.
- Dashboard ana sayfasi baslik, KPI label, filtre, bolum basliklari ve empty
state metinlerinde katalog kullanmaya basladi.
- Müşteriler, projeler, görevler, finans, takvim, günlük ve sohbet ana
header'lari katalogdan okunuyor.
- Freelancer client formatter'larinda sabit `tr-TR` kullanimi kaldirildi.
- Freelancer `date-fns/locale/tr` importlari merkezi locale mapping'e tasindi.
- `/projects` server-side sort locale-aware hale geldi.
- Faz-4 boundary script'i eklendi.
- Kalan hardcoded metin envanteri otomatik raporlanir hale geldi.
## Kalan UI metin raporu
Kalan derin form label'lari, dialog metinleri ve toast metinleri su raporda
listelendi:
- [Faz 4 hardcoded text report](i18n-phase-4-hardcoded-text-report.md)
Bu rapor Faz-4 boundary script'i tarafindan uretilir. Formatter sabitleri fail
eder; kalan kullanici metinleri sonraki UI migration dalgalarinda eritilecek
istisna envanteri olarak tutulur.
## Verification
Calistirilan komutlar:
```bash
pnpm i18n:phase4-boundary
pnpm typecheck
pnpm phase9:smoke
pnpm build
```
Sonuclar basarili. `pnpm build` sirasinda mevcut Next edge runtime static
generation uyarisi tekrar goruldu; Faz-4 kaynakli yeni bir hata degil.
File diff suppressed because it is too large Load Diff