İçindekiler
Remix i18n: Remix Uygulamaları için Uluslararasılaştırma
Remix'in sunucu öncelikli mimarisi, uluslararasılaştırmayı tamamen istemci taraflı çerçevelere kıyasla hem güçlü hem de kendine özgü kılar. Remix ile sunucuda yerel ayarı tespit edebilir, çevirileri sunucu tarafında yükleyebilir ve istemciye tamamen yerelleştirilmiş HTML gönderebilirsiniz; çevrilmemiş içerik yanıpı sönmesi yok, istemci taraflı yerel ayar tespiti gecikmeleri yok.
Bu kılavuz, Remix'te i18n'i sıfırdan nasıl uygulayacağınızı ele almaktadır: yönlendirme, yerel ayar tespiti, çeviri yükleme ve Remix için en popüler i18n kütüphanesi olan remix-i18next ile entegrasyon dahil.
Remix i18n Neden Farklıdır
Remix, loader'lar ve action'larda sunucuda kod çalıştırır ve bileşenleri hem sunucuda hem de istemcide render eder. Bu durum i18n'i önemli ölçüde değiştirir:
Sunucu taraflı çeviri yükleme: İstemci taraflı React uygulamalarında çeviriler tarayıcı tarafından yüklenir. Remix'te ise çeviriler loader fonksiyonlarında yüklenip bileşenlere aktarılabilir; bu da sunucudan çıkan HTML'in zaten çevrilmiş olduğu anlamına gelir.
URL tabanlı yönlendirme: Remix'in dosya tabanlı yönlendirmesi, /en/products ve /fr/products gibi yerel ayar tabanlı URL kalıplarıyla doğal biçimde eşleşir.
Kademeli geliştirme: Remix, JavaScript olmadan da çalışacak şekilde tasarlanmıştır. Yerelleştirilmiş rotalarınız, istemci taraflı hidrasyon olmadan doğru şekilde render edilmelidir.
İç içe yönlendirme: Remix'in iç içe düzenleri, yerel ayar tespiti ve çeviri bağlamının rota ağacının üst kısmında kurulup alt rotalara miras bırakılabilmesi anlamına gelir.
remix-i18next Kurulumu
remix-i18next, i18next üzerine inşa edilmiştir ve Remix'e özgü sunucu taraflı ile istemci taraflı i18n yardımcı programları sağlar.
Kurulum
npm install remix-i18next i18next react-i18next i18next-browser-languagedetector i18next-http-backend
Proje Yapısı
app/ ├── locales/ │ ├── en/ │ │ ├── common.json │ │ └── home.json │ └── fr/ │ ├── common.json │ └── home.json ├── i18n.server.ts # Sunucu taraflı i18n yapılandırması ├── i18n.client.ts # İstemci taraflı i18n yapılandırması ├── i18next.server.ts # remix-i18next örneği └── root.tsx
Sunucu Taraflı Yapılandırma
// app/i18n.server.ts
import { RemixI18Next } from 'remix-i18next';
import i18n from './i18n';
import Backend from 'i18next-fs-backend';
import { resolve } from 'node:path';
const i18next = new RemixI18Next({
detection: {
supportedLanguages: i18n.supportedLngs,
fallbackLanguage: i18n.fallbackLng,
},
i18next: {
...i18n,
backend: {
loadPath: resolve('./public/locales/{{lng}}/{{ns}}.json'),
},
},
plugins: [Backend],
});
export default i18next;
// app/i18n.ts - paylaşılan yapılandırma
export default {
supportedLngs: ['en', 'fr', 'de', 'ja', 'ar'],
fallbackLng: 'en',
defaultNS: 'common',
react: { useSuspense: false },
};
Kök Rota Kurulumu
// app/root.tsx
import { json, type LoaderFunctionArgs } from '@remix-run/node';
import {
Links, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData,
} from '@remix-run/react';
import { useTranslation } from 'react-i18next';
import { useChangeLanguage } from 'remix-i18next';
import i18next from '~/i18next.server';
import i18n from '~/i18n';
export async function loader({ request }: LoaderFunctionArgs) {
const locale = await i18next.getLocale(request);
return json({ locale });
}
export let handle = { i18n: 'common' };
export default function App() {
const { locale } = useLoaderData<typeof loader>();
const { i18n } = useTranslation();
useChangeLanguage(locale);
return (
<html lang={locale} dir={i18n.dir()}>
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
İstemci Taraflı Başlatma
// app/entry.client.tsx
import { startTransition, StrictMode } from 'react';
import { hydrateRoot } from 'react-dom/client';
import { RemixBrowser } from '@remix-run/react';
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
import { getInitialNamespaces } from 'remix-i18next';
import i18n from '~/i18n';
async function hydrate() {
await i18next
.use(initReactI18next)
.use(LanguageDetector)
.use(Backend)
.init({
...i18n,
ns: getInitialNamespaces(),
backend: { loadPath: '/locales/{{lng}}/{{ns}}.json' },
detection: {
order: ['htmlTag'],
caches: [],
},
});
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>
);
});
}
hydrate();
Sunucu Taraflı Giriş Noktası
// app/entry.server.tsx
import { createInstance } from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-fs-backend';
import { renderToReadableStream } from 'react-dom/server';
import { RemixServer } from '@remix-run/react';
import { resolve } from 'node:path';
import i18n from '~/i18n';
import i18next from '~/i18next.server';
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
const instance = createInstance();
const lng = await i18next.getLocale(request);
const ns = i18next.getRouteNamespaces(remixContext);
await instance
.use(initReactI18next)
.use(Backend)
.init({
...i18n,
lng,
ns,
backend: {
loadPath: resolve('./public/locales/{{lng}}/{{ns}}.json'),
},
});
const body = await renderToReadableStream(
<I18nextProvider i18n={instance}>
<RemixServer context={remixContext} url={request.url} />
</I18nextProvider>,
{
onError(error) { responseStatusCode = 500; },
}
);
responseHeaders.set('Content-Type', 'text/html');
return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}
URL Tabanlı Yerel Ayar Yönlendirmesi
Yaygın bir kalıp, yerel ayarı URL'ye kodlamaktır: /en/products, /fr/produits. Remix'te bunu bir düzen rotasıyla uygulayın:
app/routes/ ├── ($locale).tsx # Yerel ayar önekini işleyen düzen rotası ├── ($locale).index.tsx # Ana sayfa ├── ($locale).products.tsx # Ürünler sayfası └── ($locale).blog.$slug.tsx # Blog yazısı
// app/routes/($locale).tsx
import { json, redirect, type LoaderFunctionArgs } from '@remix-run/node';
import { Outlet, useLoaderData } from '@remix-run/react';
const SUPPORTED_LOCALES = ['en', 'fr', 'de', 'ja', 'ar'];
export async function loader({ params, request }: LoaderFunctionArgs) {
const { locale } = params;
// Yerel ayar öneki yok: tespit et ve yönlendir
if (!locale) {
const acceptLanguage = request.headers.get('Accept-Language') ?? 'en';
const preferredLocale = parseAcceptLanguage(acceptLanguage, SUPPORTED_LOCALES);
return redirect(`/${preferredLocale}`);
}
// Geçersiz yerel ayar: 404
if (!SUPPORTED_LOCALES.includes(locale)) {
throw new Response('Not Found', { status: 404 });
}
return json({ locale });
}
function parseAcceptLanguage(header: string, supported: string[]): string {
// Accept-Language başlığını ayrıştır ve en iyi eşleşmeyi döndür
const languages = header
.split(',')
.map(lang => lang.trim().split(';')[0].trim().split('-')[0].toLowerCase());
for (const lang of languages) {
if (supported.includes(lang)) return lang;
}
return supported[0];
}
export default function LocaleLayout() {
return <Outlet />;
}
Rota Bileşenlerinde Çevirileri Kullanma
// app/routes/($locale).products.tsx
import { json, type LoaderFunctionArgs } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import { useTranslation } from 'react-i18next';
import i18next from '~/i18next.server';
export let handle = { i18n: ['common', 'products'] };
export async function loader({ request }: LoaderFunctionArgs) {
const t = await i18next.getFixedT(request, 'products');
// SEO açısından kritik içerik için sunucu taraflı t() kullan
const title = t('page.title');
const description = t('page.description');
return json({ title, description });
}
export function meta({ data }: MetaArgs) {
return [
{ title: data?.title },
{ name: 'description', content: data?.description },
];
}
export default function ProductsPage() {
const { t } = useTranslation('products');
const { t: tc } = useTranslation('common');
return (
<main>
<h1>{t('page.title')}</h1>
<p>{t('page.description')}</p>
<a href="/">{tc('nav.home')}</a>
</main>
);
}
Dil Değiştirici Bileşeni
// app/components/LanguageSwitcher.tsx
import { Link, useLocation, useParams } from '@remix-run/react';
const LANGUAGES = [
{ code: 'en', label: 'English' },
{ code: 'fr', label: 'Français' },
{ code: 'de', label: 'Deutsch' },
{ code: 'ja', label: '日本語' },
{ code: 'ar', label: 'العربية' },
];
export function LanguageSwitcher() {
const { locale = 'en' } = useParams();
const { pathname } = useLocation();
// Mevcut yerel ayarı yolda değiştir
const getLocalePath = (newLocale: string) => {
return pathname.replace(`/${locale}`, `/${newLocale}`);
};
return (
<nav aria-label="Language selection">
{LANGUAGES.map(({ code, label }) => (
<Link
key={code}
to={getLocalePath(code)}
aria-current={code === locale ? 'page' : undefined}
hrefLang={code}
>
{label}
</Link>
))}
</nav>
);
}
RTL Desteği
Arapça ve İbranice için <html> öğesinde dir niteliğini ayarlayın:
// app/root.tsx
const RTL_LOCALES = ['ar', 'he', 'fa', 'ur'];
export default function App() {
const { locale } = useLoaderData<typeof loader>();
const { i18n } = useTranslation();
const dir = RTL_LOCALES.includes(locale) ? 'rtl' : 'ltr';
return (
<html lang={locale} dir={dir}>
{/* ... */}
</html>
);
}
Ayrıntılı CSS uygulaması için bkz. CSS ve React'te RTL desteği.
SEO Değerlendirmeleri
Çok dilli Remix uygulamaları için SEO şunları gerektirir:
<head> içinde alternatif hreflang etiketleri:
export function meta({ data, location }: MetaArgs) {
const baseUrl = 'https://example.com';
const pathWithoutLocale = location.pathname.replace(/^\/(en|fr|de|ja|ar)/, '');
return [
{ title: data?.title },
// hreflang alternatif bağlantılar
{ tagName: 'link', rel: 'alternate', hrefLang: 'en', href: `${baseUrl}/en${pathWithoutLocale}` },
{ tagName: 'link', rel: 'alternate', hrefLang: 'fr', href: `${baseUrl}/fr${pathWithoutLocale}` },
{ tagName: 'link', rel: 'alternate', hrefLang: 'x-default', href: `${baseUrl}/en${pathWithoutLocale}` },
];
}
Kapsamlı yerelleştirme SEO stratejileri için bkz. yerelleştirme SEO.
Remix'te Çoğullama
i18next, _one, _other, _zero anahtar sonekleri aracılığıyla çoğullamayı işler:
// public/locales/en/common.json
{
"items_one": "{{count}} item",
"items_other": "{{count}} items",
"items_zero": "No items"
}
const { t } = useTranslation();
t('items', { count: 0 }); // "No items"
t('items', { count: 1 }); // "1 item"
t('items', { count: 42 }); // "42 items"
Karmaşık çoğullama kurallarına sahip diller için (Rusça'nın 4, Arapça'nın 6 biçimi vardır), i18next CLDR çoğullama kurallarını otomatik olarak kullanır. Ayrıntılar için bkz. diller genelinde çoğullama kuralları.
Performans Optimizasyonu
Ad alanı bölme: handle.i18n dışa aktarımını kullanarak rota başına yalnızca gerekli ad alanlarını yükleyin. Büyük çeviri paketleri ilk yüklemeyi yavaşlatır.
Statik oluşturma: İçerik ağırlıklı siteler için, önceden render edilmiş yerelleştirilmiş sayfaları sunmak üzere Remix'in statik oluşturma özelliğini veya Cloudflare Pages'i kullanın.
Çeviri önbelleğe alma: Üretimde çeviri dosyalarını CDN düzeyinde önbelleğe alın. Çeviri dosyaları nadiren değişir ve agresif önbelleğe alma için iyi adaylardır.
Ad alanlarını tembel yükleme: Ekranın görünür alanının dışındaki içerikler için ek ad alanlarını istemcide tembel yükleyin.
Uygulamanızı better-i18n ile küreselleştirin
better-i18n; yapay zeka destekli çeviriler, git'e entegre iş akışları ve küresel CDN dağıtımını tek bir geliştirici odaklı platformda birleştirir. Elektronik tablolarla uğraşmayı bırakın ve her dilde sevkiyat yapmaya başlayın.
Ücretsiz başlayın → · Özellikleri keşfedin · Belgeleri okuyun