i18n di Production Scale
Saat aplikasi mendukung 20+ bahasa dengan ribuan translation keys, performance menjadi concern utama. Loading semua terjemahan sekaligus membunuh initial load time.
Lazy Loading Translations
// i18next: load bahasa on-demand
i18n.init({
partialBundledLanguages: true,
resources: {}, // Kosong — load via backend
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json",
},
// Hanya load bahasa aktif + fallback
preload: ["en"], // fallback selalu tersedia
});
// Namespace splitting: load per route
// React.lazy + namespace loading
const Dashboard = React.lazy(async () => {
// Load namespace sebelum render component
await i18n.loadNamespaces("dashboard");
return import("./pages/Dashboard");
});
// Route-based loading
function App() {
return (
<Routes>
<Route path="/dashboard" element={
<Suspense fallback={<Skeleton />}>
<Dashboard />
</Suspense>
} />
</Routes>
);
}
CDN untuk Translation Files
// Serve translation files dari CDN
i18n.init({
backend: {
loadPath: "https://cdn.example.com/locales/{{lng}}/{{ns}}.json",
// Cache-Control: public, max-age=31536000, immutable
// Gunakan content hash di filename untuk cache busting
},
});
// Build step: hash translation files
// locales/id/common.abc123.json
// manifest.json: { "id/common": "abc123" }
// Atau gunakan service worker untuk cache
self.addEventListener("fetch", (event) => {
if (event.request.url.includes("/locales/")) {
event.respondWith(
caches.match(event.request).then(
(cached) => cached || fetch(event.request).then((response) => {
const cache = await caches.open("i18n-v1");
cache.put(event.request, response.clone());
return response;
})
)
);
}
});
Bundle Size Optimization
// Masalah: Intl polyfills bisa besar
// @formatjs/intl-pluralrules — 50KB+ dengan semua locale
// Solusi: hanya load locale yang dibutuhkan
import "@formatjs/intl-pluralrules/polyfill";
import "@formatjs/intl-pluralrules/locale-data/id"; // Hanya Indonesia
import "@formatjs/intl-pluralrules/locale-data/en"; // Hanya English
// Dynamic import berdasarkan locale
async function loadPolyfills(locale) {
if (!Intl.PluralRules) {
await import("@formatjs/intl-pluralrules/polyfill");
}
await import(`@formatjs/intl-pluralrules/locale-data/${locale}`);
}
Server-side Caching
// Laravel: cache terjemahan per locale
// Jangan load file terjemahan setiap request
// Artisan command
php artisan lang:cache // Cache semua translation files
php artisan lang:clear // Clear cache
// Custom caching untuk dynamic translations (dari database)
class TranslationService
{
public function get(string $key, string $locale): string
{
return Cache::remember(
"translations.{$locale}.{$key}",
now()->addHours(24),
fn () => Translation::where("key", $key)
->where("locale", $locale)
->value("value") ?? $key
);
}
public function clearCache(string $locale): void
{
Cache::tags(["translations", $locale])->flush();
}
}
Monitoring & Analytics
// Track missing translations di production
i18n.on("missingKey", (lngs, namespace, key) => {
// Kirim ke error tracking (Sentry, etc)
captureMessage("Missing i18n key", {
extra: { languages: lngs, namespace, key },
level: "warning",
});
});
// Track language distribution
analytics.track("language_selected", {
locale: i18n.language,
source: "auto" | "manual",
});
// Monitor translation coverage
// Dashboard: % translated per locale
// Alert jika coverage < 90%
Performance Checklist
- Lazy load — Hanya muat bahasa yang aktif, bukan semua
- Namespace splitting — Pisahkan terjemahan per route/feature
- CDN — Serve translation files dari edge server
- Cache — Browser cache + service worker + server cache
- Tree-shake — Import hanya locale data yang dibutuhkan
- Compress — Gzip/Brotli translation files (JSON compresses well)
- Preload — <link rel="preload"> untuk locale yang kemungkinan dipakai