feat: expose mobile localization contracts
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"scenario": "English mobile client receives resolved user locale from /me",
|
||||
"request": {
|
||||
"path": "/api/v1/me?locale=en",
|
||||
"headers": {
|
||||
"Accept-Language": "en-US,en;q=0.9,tr;q=0.7",
|
||||
"Cookie": "better-auth.session_token=<redacted>"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"data": {
|
||||
"localization": {
|
||||
"language": "en",
|
||||
"portalLocale": null,
|
||||
"resolvedLocale": "en",
|
||||
"requestedLocale": "en",
|
||||
"defaultLocale": "tr",
|
||||
"source": "query",
|
||||
"fallbackChain": ["en", "tr"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"scenario": "French is requested before the self-host instance activates it",
|
||||
"request": {
|
||||
"path": "/api/v1/me?locale=fr",
|
||||
"headers": {
|
||||
"Accept-Language": "fr-FR,fr;q=0.9,en;q=0.8,tr;q=0.7",
|
||||
"Cookie": "better-auth.session_token=<redacted>"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": "UNSUPPORTED_LOCALE",
|
||||
"details": {
|
||||
"requestedLocale": "fr"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"scenario": "Turkish mobile client discovers localization support",
|
||||
"request": {
|
||||
"path": "/api/v1/meta",
|
||||
"headers": {
|
||||
"Accept-Language": "tr-TR,tr;q=0.9,en;q=0.7"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"data": {
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "instance.localization",
|
||||
"version": 1,
|
||||
"status": "available",
|
||||
"access": "public"
|
||||
}
|
||||
],
|
||||
"localization": {
|
||||
"defaultLocale": "tr",
|
||||
"negotiator": {
|
||||
"queryParam": "locale",
|
||||
"header": "Accept-Language",
|
||||
"unsupportedLocaleCode": "UNSUPPORTED_LOCALE",
|
||||
"matching": "exact-or-base-language"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
title: I18n Faz 8 — API v1 ve mobil hazırlık
|
||||
status: completed
|
||||
completed_at: 2026-07-19
|
||||
---
|
||||
|
||||
# I18n Faz 8 — API v1 ve mobil hazırlık
|
||||
|
||||
Bu fazda self-host instance içindeki dil modeli, gelecekteki React Native
|
||||
istemcilerinin güvenli şekilde keşfedebileceği bir API v1 sözleşmesine
|
||||
taşındı. Değişiklikler geriye uyumludur; mevcut v1 response alanları
|
||||
değiştirilmedi, sadece additive alanlar eklendi.
|
||||
|
||||
## Tamamlananlar
|
||||
|
||||
- `instance.localization` capability kaydı eklendi.
|
||||
- `/.well-known/neta` discovery document içine additive locale özeti eklendi.
|
||||
- `/api/v1/meta` içinde localization contract genişletildi:
|
||||
- `supportedLocales`
|
||||
- `fallbacks`
|
||||
- `catalogVersion`
|
||||
- `negotiator`
|
||||
- `responseContract`
|
||||
- `/api/v1/me` response'u kullanıcının dil bilgisini ayrı döner:
|
||||
- `language`
|
||||
- `portalLocale`
|
||||
- `resolvedLocale`
|
||||
- `requestedLocale`
|
||||
- `source`
|
||||
- `fallbackChain`
|
||||
- `Accept-Language` parser ve locale negotiation helper'ı eklendi.
|
||||
- Gelecek resource endpoint'leri için localized response contract'ı yazıldı.
|
||||
- Owner mutation contract'ında `translations` shape'i standartlaştırıldı:
|
||||
`Record<locale, Record<field, string | null>>`.
|
||||
- `UNSUPPORTED_LOCALE` hata kodu API response mapping'e eklendi.
|
||||
- API hata response'larında client localization için `messageKey` kullanımı
|
||||
netleştirildi.
|
||||
- `tr`, `en`, `fr` için contract fixture'ları eklendi.
|
||||
- `i18n:phase8-smoke` mobile localization negotiation ve contract shape'lerini
|
||||
runtime olarak doğrulayacak şekilde eklendi.
|
||||
|
||||
## Mobil istemci davranışı
|
||||
|
||||
Mobil istemci ilk açılışta `/.well-known/neta` endpoint'ine gider ve
|
||||
`instance.localization` capability'sini kontrol eder. Capability bilinmiyorsa
|
||||
istemci bunu fatal hata olarak ele almamalı; capability listesi additive olduğu
|
||||
için unknown capability değerleri yok sayılmalıdır.
|
||||
|
||||
Dil seçimi için önerilen sıra:
|
||||
|
||||
1. Kullanıcının explicit seçimi varsa `/api/v1/me?locale=xx` ile gönder.
|
||||
2. Explicit seçim yoksa `Accept-Language` header'ını gönder.
|
||||
3. Server `/api/v1/me.data.localization.resolvedLocale` değerini gerçek kaynak
|
||||
kabul et.
|
||||
|
||||
`locale` query param'ı aktif olmayan bir locale'e işaret ederse API
|
||||
`UNSUPPORTED_LOCALE` döner. `Accept-Language` içinde desteklenmeyen değer varsa
|
||||
server sessizce instance default locale'e düşebilir.
|
||||
|
||||
Hata response'larında kullanıcıya gösterilecek metin mobile client tarafından
|
||||
locale'e göre çözülmelidir. Server bu amaçla `error.details.messageKey`
|
||||
alanını döndürür:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": "UNSUPPORTED_LOCALE",
|
||||
"message": "Unsupported locale.",
|
||||
"details": {
|
||||
"messageKey": "validation.unsupportedLocale",
|
||||
"requestedLocale": "fr"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Resource response contract
|
||||
|
||||
Gelecek `/api/v1/projects`, `/api/v1/tasks`, `/api/v1/clients` gibi resource
|
||||
endpoint'leri şu shape'i kullanmalı:
|
||||
|
||||
```ts
|
||||
{
|
||||
resource: TResource;
|
||||
localized: TResource;
|
||||
locale: string;
|
||||
fallbackChain: string[];
|
||||
}
|
||||
```
|
||||
|
||||
Bu contract sayesinde mobil taraf original kaydı ve locale çözülmüş kaydı aynı
|
||||
anda taşıyabilir.
|
||||
|
||||
## Mutation translations contract
|
||||
|
||||
Owner/freelancer mutation endpoint'leri çok dilli alanları şu shape ile kabul
|
||||
etmeli:
|
||||
|
||||
```ts
|
||||
{
|
||||
translations: {
|
||||
tr: { name: "Marka sitesi", description: "..." },
|
||||
en: { name: "Brand website", description: "..." },
|
||||
fr: { name: "Site de marque", description: "..." }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Server sadece authorized owner mutation'larında bu alanı kabul eder. Müşteri
|
||||
portal oturumu `translations` mutation contract'ını kullanamaz; portal locale
|
||||
yalnızca kendi okuma response'unu etkiler.
|
||||
|
||||
## Cache ve URL notları
|
||||
|
||||
- `/.well-known/neta` ve `/api/v1/meta` public cache kullanır.
|
||||
- Metadata içindeki URL'ler `APP_URL` üzerinden absolute üretilir.
|
||||
- Locale catalog değişikliklerinde `catalogVersion` artacağı için mobil istemci
|
||||
meta cache'ini güvenli şekilde invalidate edebilir.
|
||||
|
||||
## Fixture'lar
|
||||
|
||||
- `docs/self-hosted-redesign/i18n-phase-8-fixtures/api-v1-locale-tr.json`
|
||||
- `docs/self-hosted-redesign/i18n-phase-8-fixtures/api-v1-locale-en.json`
|
||||
- `docs/self-hosted-redesign/i18n-phase-8-fixtures/api-v1-locale-fr.json`
|
||||
|
||||
## Verification
|
||||
|
||||
Çalıştırılan komutlar:
|
||||
|
||||
```bash
|
||||
pnpm i18n:phase8-smoke
|
||||
pnpm typecheck
|
||||
pnpm build
|
||||
pnpm phase9:smoke
|
||||
git diff --check
|
||||
```
|
||||
@@ -85,17 +85,23 @@ Hata:
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": "UNAUTHENTICATED",
|
||||
"message": "Geçerli bir oturum gerekli.",
|
||||
"details": {}
|
||||
"message": "Authentication required.",
|
||||
"details": {
|
||||
"messageKey": "api.errors.unauthenticated"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`details` opsiyoneldir. `/api/v1` yanıtları `X-Neta-API-Version: 1` header'ı taşır. Mobil istemci kullanıcıya göstereceği metni `message` alanından alabilir; program akışını yalnızca stabil `code` üzerinden kurmalıdır.
|
||||
`details` opsiyoneldir. `/api/v1` yanıtları `X-Neta-API-Version: 1` header'ı taşır.
|
||||
Mobil istemci program akışını yalnızca stabil `code` üzerinden kurmalı, kullanıcıya
|
||||
göstereceği metni mümkünse `details.messageKey` ile kendi catalog'undan çözmelidir.
|
||||
`message` alanı debug/fallback içindir ve lokalizasyon kaynağı kabul edilmemelidir.
|
||||
|
||||
Mevcut hata kodları:
|
||||
|
||||
- `VALIDATION_ERROR`
|
||||
- `UNSUPPORTED_LOCALE`
|
||||
- `UNAUTHENTICATED`
|
||||
- `FORBIDDEN`
|
||||
- `NOT_FOUND`
|
||||
|
||||
Reference in New Issue
Block a user