fix: parse raw public translation responses in banner
Some checks failed
CI / Banner Lint & Typecheck (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / API Lint (push) Has been cancelled
CI / API Tests (push) Has been cancelled
CI / Scanner Lint (push) Has been cancelled
CI / Scanner Tests (push) Has been cancelled
CI / Banner Tests (push) Has been cancelled
CI / Banner Build (push) Has been cancelled
CI / Admin UI Typecheck (push) Has been cancelled
CI / Admin UI Tests (push) Has been cancelled
CI / Admin UI Build (push) Has been cancelled
Some checks failed
CI / Banner Lint & Typecheck (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / API Lint (push) Has been cancelled
CI / API Tests (push) Has been cancelled
CI / Scanner Lint (push) Has been cancelled
CI / Scanner Tests (push) Has been cancelled
CI / Banner Tests (push) Has been cancelled
CI / Banner Build (push) Has been cancelled
CI / Admin UI Typecheck (push) Has been cancelled
CI / Admin UI Tests (push) Has been cancelled
CI / Admin UI Build (push) Has been cancelled
The public translations endpoint returns the raw strings dictionary, but
the banner expected a wrapped { strings } response and therefore fell back
to English for Thai translations.
Update fetchTranslations() to accept the raw public API shape while keeping
compatibility with wrapped responses. Make default_language optional in the
banner SiteConfig type so old/mocked configs still typecheck.
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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<TranslationStrings> };
|
||||
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<TranslationStrings> | { strings?: Partial<TranslationStrings> };
|
||||
return 'strings' in data && data.strings ? data.strings : data as Partial<TranslationStrings>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export interface SiteConfig {
|
||||
gcm_default: Record<string, 'granted' | 'denied'> | 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;
|
||||
|
||||
Reference in New Issue
Block a user