Teraz z AI - twórz strony przez serwer MCP

i18n

Każde pole jest domyślnie wielojęzyczne. Jak locale kształtują URL-e, do którego trafia żądanie i co się dzieje przy braku tłumaczenia.

W cmssy nie modelujesz tłumaczeń. Każde pole jest już wielojęzyczne: treść jest przechowywana z kluczem kodu języka, więc jedna strona niesie wszystkie swoje języki, a jedna instancja bloku wszystkie swoje.

To usuwa typowy problem duplikacji - nie ma "strony angielskiej" i "strony niemieckiej" do synchronizowania - i zastępuje go dwoma mniejszymi pytaniami. Do którego locale rozwiązuje się żądanie i co się pokazuje, gdy pole nie jest jeszcze przetłumaczone?

Jedna rzecz do ustalenia przed kodem poniżej: od odchudzonego wydania 10.x SDK nie dostarcza żadnych helperów do locale. resolveSiteLocales, localizedPath, splitLocaleFromPath i pickLocalized w tych snippetach są Twoje własne - jakieś czterdzieści linijek w lib/, współdzielone przez route catch-all, sitemapę i generateMetadata. Reguły, które implementują, należą do cmssy; kod należy do Ciebie. (Odpowiedniki żyją pod @cmssy/core/internal, ale "internal" znaczy, że mogą się przenieść bez adnotacji o breaking change.)

Zestaw locale

Locale pochodzą z konfiguracji serwisu, nie z Twojego kodu:

const { defaultLocale, locales } = await resolveSiteLocales();

Dodanie języka to ustawienie workspace'u. Twoja aplikacja podchwyci je przy następnym żądaniu, co oznacza, że nowe locale nie wymaga deployu - tylko tłumaczeń.

URL-e: domyślne locale nie ma prefiksu

To reguła, z której wynika cała reszta:

localizedPath("/pricing", "pl", "pl"); // "/pricing"    domyślne: gołe
 localizedPath("/pricing", "de", "pl"); // "/de/pricing" pozostałe: z prefiksem
localizedPath("/", "de", "pl");        // "/de"

Trzymanie domyślnego locale bez prefiksu oznacza, że Twoje kanoniczne URL-e nie zmieniają się przy dodaniu drugiego języka - istniejące linki, udostępnienia i pozycje w wyszukiwarce przeżyją.

Rozwiązywanie żądania

Kierunek odwrotny czyta pierwszy segment ścieżki i jest celowo restrykcyjny:

splitLocaleFromPath(["de", "pricing"], { defaultLocale: "pl", locales: ["pl", "de"] });
// -> { locale: "de", path: ["pricing"] }

splitLocaleFromPath(["pricing"], { defaultLocale: "pl", locales: ["pl", "de"] });
// -> { locale: "pl", path: ["pricing"] }

Pierwszy segment liczy się jako locale tylko wtedy, gdy jest w locales i nie jest domyślny. Więc /pl/pricing to nie polska strona cennika - to strona, której slug przypadkiem zaczyna się od pl. Jeden kanoniczny URL na stronę na język, zero duplikatów.

Catch-all route robi to przed wyszukaniem sluga:

const { path: strippedPath } = splitLocaleFromPath(path, await resolveSiteLocales());
const slug = "/" + (strippedPath ?? []).join("/");

Brakujące tłumaczenia

Pole spada do fallbacku, a nie znika:

pickLocalized(value, locale, defaultLocale);
// value[locale] ?? value[defaultLocale] ?? pierwsza dostępna wartość

Trzy kroki: żądany język, potem domyślny, potem cokolwiek istnieje. Ten ostatni ma znaczenie - pole przetłumaczone tylko na francuski dalej się renderuje na stronie niemieckiej, zamiast zostawiać dziurę.

Konsekwencja dla autorów bloków: częściowo przetłumaczona strona to stan normalny, nie błąd. Pisz komponenty renderujące się sensownie, gdy pole wróci w niewłaściwym języku, i testuj z locale, którego nie przetłumaczyłeś - to pierwsze, na co trafią odwiedzający w nowym języku.

Middleware

// proxy.ts
export const proxy = createCmssyProxy(cmssy);

// Route'y statyczne zamiast catch-alla? Wtedy i tylko wtedy:
// createCmssyProxy(cmssy, { stripLocalePrefix: true });

SDK mówi to wprost: zostaw wyłączone dla aplikacji z catch-allem, która sama odczytuje język ze ścieżki. Włącz mimo to, a route dostanie ścieżkę bez prefiksu, więc splitLocaleFromPath nie znajdzie czego ścinać i spadnie do domyślnego locale - każdy język wyrenderuje się jako domyślny.

Middleware nadal rozwiązuje język i przekazuje go dalej w nagłówku, więc aplikacja może odczytać go stamtąd. Ale jeśli Twój route wyprowadza locale ze ścieżki, jak zwykle robi catch-all, to te dwa źródła mówią mu teraz co innego.

Zadeklaruj język w HTML-u

Ustaw <html lang> z rozwiązanego locale. To jest to, co czytają czytniki ekranu i narzędzia tłumaczące, i jest to kontrakt - dlatego smoke test edytora sprawdza właśnie to, a nie szuka słowa w Twojej treści. Treść to treść; redaktor może ją zmienić w dowolnej chwili, i wtedy test kłamie.

Następne kroki

  • SEO - alternatywy hreflang budowane z tego samego zestawu locale.
  • Testowanie - asercja na <html lang>.
  • Modele danych - rekordy też są wielojęzyczne.