Files
ipal-kit/docs/i18n.md
T

5.8 KiB

i18n

Lokalizacja: konfiguracja locali dla Payload, negocjacja języka (cookie / Accept-Language / default), budowanie ścieżek locale-aware, przełączanie języka bez 404.

Config (payload.config.ts)

ipalKit({
  i18n: {
    defaultLocale: 'pl',
    locales: [
      { code: 'pl', label: 'Polski' },
      { code: 'en', label: 'English' },
      // { code: 'ar', label: 'العربية', rtl: true },
    ],
    // fallback: true,  // domyślnie true
  },
})

Plugin ustawia config.localization z tego. Walidacja jest eager (fail-fast przy starcie): pusta lista, duplikaty kodów, defaultLocale spoza listy, zły format kodu → błąd [ipal] i18n: ....

Front — helpery

Wszystkie helpery są czyste (przyjmują config jako argument). Trzymaj swój i18nConfig w jednym miejscu i importuj gdzie trzeba.

import {
  getLocaleCodes, getDefaultLocale, isValidLocale, getLocaleDefinition,
  negotiateLocale, buildLocalizedPath, switchLocalePath,
  getLocalizedSlugs, LOCALE_COOKIE_NAME,
} from '@intecion/ipal-kit'

const config = { defaultLocale: 'pl', locales: [{code:'pl',label:'Polski'},{code:'en',label:'English'}] }

getLocaleCodes(config)              // ['pl', 'en']
isValidLocale('de', config)         // false

Budowanie ścieżek

// dokument pobrany z locale:'all' → slug to mapa { pl, en }
const doc = await payload.findByID({ collection: 'pages', id, locale: 'all' })
const slugs = getLocalizedSlugs({ slugField: doc.slug, config })
// { pl: 'o-nas', en: 'about' }

buildLocalizedPath({ slugs, locale: 'en', config })   // '/en/about'
// home slug ('home') zwija się do roota:
buildLocalizedPath({ slugs: { en: 'home' }, locale: 'en', config })  // '/en'

Pułapka typu (TypeScript): przy locale: 'all' Payload w RUNTIME zwraca zlokalizowane pole jako obiekt { pl, en }, ale wygenerowane typy Payloada deklarują doc.slug jako string (typ nie odróżnia trybu all). tsc zgłosi więc niezgodność. Rozwiązanie — czyste rzutowanie na oczekiwany przez helper typ:

const slugs = getLocalizedSlugs({
  slugField: doc.slug as unknown as Record<string, unknown>,
  config,
})

To nie hack — to pomost między statycznym typem (string) a rzeczywistym kształtem runtime (obiekt), którego generator typów Payloada nie modeluje. as unknown as jest tu poprawne, bo TS nie zna trybu all.

Przełącznik języka (bez 404)

// /pl/strona-glowna → klik EN → /en/home (albo /en jeśli brak tłumaczenia)
const href = switchLocalePath({ slugs, targetLocale: 'en', config })
router.push(href)

switchLocalePath nigdy nie zwraca undefined — brak slug w danym locale → fallback na /{locale} (root), zamiast dead-endu na 404.

Middleware — patrz osobno

Negocjacja locale + redirect na wejściu (domena.com → /pl) jest w @intecion/ipal-kit/next/middleware. Zobacz proxy w tej sekcji niżej.

Proxy (dawniej middleware)

Next 16: plik nazywa się teraz proxy.ts, funkcja proxy. Import z pluginu (@intecion/ipal-kit/next/middleware) bez zmian — to nazwa subpath eksportu, niezależna od nazwy pliku projektu. Migracja: npx @next/codemod@canary middleware-to-proxy .

// proxy.ts (projekt klienta) — jedyna logika to podłączenie
import { NextResponse } from 'next/server'
import { createLocaleMiddleware, DEFAULT_MIDDLEWARE_MATCHER } from '@intecion/ipal-kit/next/middleware'

const i18nConfig = {
  defaultLocale: 'pl',
  locales: [{ code: 'pl', label: 'Polski' }, { code: 'en', label: 'English' }],
}
const localeMiddleware = createLocaleMiddleware({ config: i18nConfig })

export function proxy(req) {
  const r = localeMiddleware(req)
  if (r.type === 'next') return NextResponse.next()
  const res = NextResponse.redirect(r.location)
  if (r.cookie) res.cookies.set(r.cookie.name, r.cookie.value)
  return res
}

export const config = { matcher: DEFAULT_MIDDLEWARE_MATCHER }

Zachowanie:

  • ścieżka z locale (/pl/...) → przepuść
  • root albo ścieżka bez locale (/, /o-nas) → redirect na /{locale}..., locale z: cookie → Accept-Language → default
  • wybrany locale zapisany w cookie (LOCALE_COOKIE_NAME)

DEFAULT_MIDDLEWARE_MATCHER wyklucza api, admin, _next, pliki statyczne.

Wybór języka zapisywany jest w cookie NEXT_LOCALE (konwencja Next.js — kompatybilna z innymi bibliotekami i18n, które czytają aktywny locale). Ale zapis podlega zgodzie: to cookie kategorii functional, więc:

  • Zapis TYLKO za zgodą. Middleware zapisuje NEXT_LOCALE jedynie, gdy użytkownik zgodził się na kategorię functional (mayPersistLocale sprawdza zgodę). Bez zgody język działa per żądanie (negocjacja z Accept-Language), ale nie jest utrwalany.
  • Sprzątanie po cofnięciu zgody. Gdy użytkownik cofnie zgodę na functional, cookie NEXT_LOCALE jest usuwane automatycznie (consent zna tę cookie przez DEFAULT_COOKIE_MAP — patrz consent.md).

Nazwa cookie to jedna stała LOCALE_COOKIE_NAME (modules/i18n/negotiateLocale), propagująca do middleware i sprzątania consent. Można nadpisać w createLocaleMiddleware({ cookieName }), ale domyślnie NEXT_LOCALE jest zalecane (interop).

Kolejność negocjacji locale

  1. Cookie NEXT_LOCALE (jeśli jest — czyli był wybór za zgodą)
  2. Nagłówek Accept-Language (preferencje przeglądarki)
  3. defaultLocale z konfiguracji

Wejście na / → negocjacja → redirect na /pl (albo wynik negocjacji). Zmiana języka (URL /en różny od cookie) → zapis nowego wyboru (za zgodą).

Migracja ze starej nazwy: wcześniej cookie nazywało się ipal-locale. Po zmianie na NEXT_LOCALE użytkownicy ze starą cookie przejdą raz ponowną negocjację (stara cookie ignorowana). Jednorazowe, bez wpływu na nowe projekty.