diff --git a/apps/banner/src/__tests__/i18n.test.ts b/apps/banner/src/__tests__/i18n.test.ts index 3e0a94e..6917523 100644 --- a/apps/banner/src/__tests__/i18n.test.ts +++ b/apps/banner/src/__tests__/i18n.test.ts @@ -106,9 +106,9 @@ describe('i18n', () => { json: () => Promise.resolve(mockTranslations), })); - const result = await fetchTranslations('https://cdn.example.com', 'fr'); + const result = await fetchTranslations('https://api.example.com', 'site-123', 'fr'); - expect(fetch).toHaveBeenCalledWith('https://cdn.example.com/translations-fr.json'); + expect(fetch).toHaveBeenCalledWith('https://api.example.com/api/v1/translations/site-123/fr'); expect(result).toEqual(mockTranslations); vi.unstubAllGlobals(); @@ -117,7 +117,7 @@ describe('i18n', () => { it('should return null on 404', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 404 })); - const result = await fetchTranslations('https://cdn.example.com', 'zz'); + const result = await fetchTranslations('https://api.example.com', 'site-123', 'zz'); expect(result).toBeNull(); vi.unstubAllGlobals(); @@ -126,7 +126,7 @@ describe('i18n', () => { it('should return null on network error', async () => { vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('Network error'))); - const result = await fetchTranslations('https://cdn.example.com', 'fr'); + const result = await fetchTranslations('https://api.example.com', 'site-123', 'fr'); expect(result).toBeNull(); vi.unstubAllGlobals(); @@ -140,7 +140,23 @@ describe('i18n', () => { expect(t.acceptAll).toBe(DEFAULT_TRANSLATIONS.acceptAll); }); - it('should merge remote translations over defaults', async () => { + it('should merge raw public API translations over defaults', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ title: 'เราใช้คุกกี้', acceptAll: 'ยอมรับทั้งหมด' }), + })); + + const t = await loadTranslations('https://api.example.com', 'site-123', 'th'); + + expect(t.title).toBe('เราใช้คุกกี้'); + expect(t.acceptAll).toBe('ยอมรับทั้งหมด'); + // Missing keys should fall back to English + expect(t.rejectAll).toBe(DEFAULT_TRANSLATIONS.rejectAll); + + vi.unstubAllGlobals(); + }); + + it('should also accept legacy wrapped translation responses', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ strings: { title: 'Wir verwenden Cookies', acceptAll: 'Alle akzeptieren' } }), @@ -150,8 +166,6 @@ describe('i18n', () => { expect(t.title).toBe('Wir verwenden Cookies'); expect(t.acceptAll).toBe('Alle akzeptieren'); - // Missing keys should fall back to English - expect(t.rejectAll).toBe(DEFAULT_TRANSLATIONS.rejectAll); vi.unstubAllGlobals(); }); diff --git a/apps/banner/src/i18n.ts b/apps/banner/src/i18n.ts index 8df841d..b7a7a81 100644 --- a/apps/banner/src/i18n.ts +++ b/apps/banner/src/i18n.ts @@ -95,9 +95,10 @@ export async function fetchTranslations( try { const resp = await fetch(`${apiBase}/api/v1/translations/${siteId}/${locale}`); if (!resp.ok) return null; - // API returns { strings: { ... } } - const data = (await resp.json()) as { strings?: Partial }; - return data.strings ?? null; + // Public API returns the raw strings dict. Accept a wrapped `{ strings }` + // shape too for backwards compatibility with older mocks/clients. + const data = await resp.json() as Partial | { strings?: Partial }; + return 'strings' in data && data.strings ? data.strings : data as Partial; } catch { return null; } diff --git a/apps/banner/src/types.ts b/apps/banner/src/types.ts index c517947..7c458d8 100644 --- a/apps/banner/src/types.ts +++ b/apps/banner/src/types.ts @@ -80,7 +80,7 @@ export interface SiteConfig { gcm_default: Record | null; shopify_privacy_enabled: boolean; banner_config: BannerConfig | null; - default_language: string | null; + default_language?: string | null; privacy_policy_url: string | null; terms_url: string | null; consent_expiry_days: number;