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.