Files
portfolio/lib/i18n-loader.ts
denshooter bdf02b2a3a
All checks were successful
CI / CD / test-build (push) Successful in 10m11s
CI / CD / deploy-dev (push) Successful in 1m17s
CI / CD / deploy-production (push) Has been skipped
fix: eliminate 2s LCP rendering delay from Directus timeout on Hero
The Hero server component awaited getMessages(locale) which called Directus
with a 2-second timeout. On testing.dk0.dev (or when Directus is unreachable),
this blocked the entire Hero render for ~2s → LCP 3.0s / 2320ms rendering delay.

Changes:
- Hero.tsx: remove getMessages() call entirely; use t() for all strings
- messages/en.json + de.json: add hero.badge, hero.line1, hero.line2 keys
- lib/i18n-loader.ts: invert lookup order — JSON first, Directus only as
  override for keys absent from JSON. Previously Directus was tried first
  for every key, causing ~49 parallel network requests per page load in
  HomePageServer (aboutT + projectsT + contactT + footerT translations).
  Now all JSON-backed keys return instantly without any network I/O.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 22:36:03 +01:00

143 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* i18n Loader with Directus + JSON Fallback
* - Fetches from Directus first
* - Falls back to JSON files if not found
* - Caches results (5 min TTL)
*/
import { getMessage, getContentPage, ContentPage } from './directus';
import enMessages from '@/messages/en.json';
import deMessages from '@/messages/de.json';
const jsonFallback = { en: enMessages, de: deMessages };
// Simple in-memory cache
const cache = new Map<string, { value: unknown; expires: number }>();
function setCached(key: string, value: unknown, ttlSeconds = 300) {
cache.set(key, { value, expires: Date.now() + ttlSeconds * 1000 });
}
function getCached(key: string): unknown | null {
const hit = cache.get(key);
if (!hit) return null;
if (Date.now() > hit.expires) {
cache.delete(key);
return null;
}
return hit.value;
}
/**
* Get a localized message by key.
* Tries: JSON (requested locale) → JSON (EN) → Directus (requested locale) → Directus (EN)
*
* JSON is checked first so that translation-heavy server components never wait on
* a Directus network round-trip for keys that already exist in the message files.
* Directus is only queried when the key is absent from JSON (i.e. CMS-only content).
*/
export async function getLocalizedMessage(
key: string,
locale: string
): Promise<string> {
const cacheKey = `msg:${key}:${locale}`;
const cached = getCached(cacheKey);
if (cached !== null) return cached as string;
const normalizedLocale = locale.startsWith('de') ? 'de' : 'en';
// 1) JSON requested locale
const jsonValue = getNestedValue(jsonFallback[normalizedLocale as 'en' | 'de'], key);
if (jsonValue) {
setCached(cacheKey, jsonValue);
return jsonValue;
}
// 2) JSON EN fallback
if (normalizedLocale !== 'en') {
const jsonValueEn = getNestedValue(jsonFallback['en'], key);
if (jsonValueEn) {
setCached(cacheKey, jsonValueEn);
return jsonValueEn;
}
}
// 3) Directus only for keys missing from JSON (CMS-only content)
const dbValue = await getMessage(key, locale);
if (dbValue) {
setCached(cacheKey, dbValue);
return dbValue;
}
// 4) Directus EN fallback
if (locale !== 'en') {
const dbValueEn = await getMessage(key, 'en');
if (dbValueEn) {
setCached(cacheKey, dbValueEn);
return dbValueEn;
}
}
// 5) Return the key itself as last resort
return key;
}
/**
* Get a localized content page by slug
* Tries: Directus (requested locale) → Directus (EN)
*/
export async function getLocalizedContent(
slug: string,
locale: string
): Promise<ContentPage | null> {
const cacheKey = `page:${slug}:${locale}`;
const cached = getCached(cacheKey);
if (cached !== null) return cached as ContentPage;
if (cache.has(cacheKey)) return null; // Already checked, not found
// Try Directus with requested locale
const dbPage = await getContentPage(slug, locale);
if (dbPage) {
setCached(cacheKey, dbPage);
return dbPage;
}
// Fallback to EN in Directus
if (locale !== 'en') {
const dbPageEn = await getContentPage(slug, 'en');
if (dbPageEn) {
setCached(cacheKey, dbPageEn);
return dbPageEn;
}
}
// Not found
setCached(cacheKey, null);
return null;
}
/**
* Helper: Get nested value from object
* Example: "nav.home" → obj.nav.home
*/
function getNestedValue(obj: Record<string, unknown>, path: string): string | null {
const keys = path.split('.');
let value: unknown = obj;
for (const key of keys) {
if (value && typeof value === 'object' && key in value) {
value = (value as Record<string, unknown>)[key];
} else {
return null;
}
if (value === undefined) return null;
}
return typeof value === 'string' ? value : null;
}
/**
* Clear cache (useful for webhooks/revalidation)
*/
export function clearI18nCache() {
cache.clear();
}