perf(i18n): load the English fallback lazily, off the startup critical path
`initI18n` runs in the client `init()` hook and blocks the first render. For every non-English user it awaited TWO locale dictionaries back to back — the active locale AND `en` (the fallback) — so first paint waited on two sequential round-trips + JSON parses. Now it awaits only the active locale, then warms `en` in the background (non-blocking). `t()` only consults `dicts.en` for keys the active locale is missing, and most call sites already pass an inline English fallback, so the deferred `en` doesn't change what users see; when it arrives `dicts.en` is reactive, so any key that fell through re-renders. English users are unchanged (no second fetch was ever needed). Net: non-English startup drops from two blocking locale fetches to one, halving the i18n payload on the critical path (the server already serves these JSONs brotli/gzip-compressed via the global CompressionLayer, so the wire cost was already small — this removes the extra round-trip + parse from first paint). Validated: new unit test (initI18n resolves while the en fetch is still pending, en is kicked off in the background, and a key missing from the active locale falls back once en lands) → 47 frontend tests green; npm run check; prod build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8Vb9QHmLZnEMzHz7MrFy6
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getNestedValue, interpolate, resolveBrowserLocale } from './index.svelte';
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
import {
|
||||
getNestedValue,
|
||||
interpolate,
|
||||
resolveBrowserLocale,
|
||||
initI18n,
|
||||
t,
|
||||
i18n
|
||||
} from './index.svelte';
|
||||
|
||||
describe('resolveBrowserLocale', () => {
|
||||
it('matches an exact full tag', () => {
|
||||
@@ -69,3 +76,44 @@ describe('interpolate', () => {
|
||||
expect(interpolate('{{count}} items', { count: 5 })).toBe('5 items');
|
||||
});
|
||||
});
|
||||
|
||||
describe('initI18n — lazy English fallback', () => {
|
||||
let resolveEn: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.setItem('oxicloud-locale', 'es');
|
||||
resolveEn = () => {};
|
||||
globalThis.fetch = vi.fn((input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url.includes('/es.json')) {
|
||||
return Promise.resolve(new Response(JSON.stringify({ greeting: 'Hola' }), { status: 200 }));
|
||||
}
|
||||
if (url.includes('/en.json')) {
|
||||
// Deferred: only resolves when the test flips it, proving init didn't wait.
|
||||
return new Promise<Response>((res) => {
|
||||
resolveEn = () =>
|
||||
res(new Response(JSON.stringify({ only_en: 'English only' }), { status: 200 }));
|
||||
});
|
||||
}
|
||||
return Promise.resolve(new Response('{}', { status: 404 }));
|
||||
}) as unknown as typeof fetch;
|
||||
});
|
||||
|
||||
it('is ready after only the active locale and warms en in the background', async () => {
|
||||
// Resolves even though the en fetch is still pending — it isn't awaited.
|
||||
await initI18n();
|
||||
expect(i18n.loaded).toBe(true);
|
||||
expect(i18n.locale).toBe('es');
|
||||
expect(t('greeting')).toBe('Hola');
|
||||
|
||||
const urls = vi.mocked(globalThis.fetch).mock.calls.map((c) => String(c[0]));
|
||||
expect(urls.some((u) => u.includes('/es.json'))).toBe(true);
|
||||
expect(urls.some((u) => u.includes('/en.json'))).toBe(true); // en was kicked off
|
||||
|
||||
// A key missing from es is unresolved until en arrives, then falls back.
|
||||
expect(t('only_en')).toBe('only_en');
|
||||
resolveEn();
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect(t('only_en')).toBe('English only');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -207,9 +207,13 @@ export async function initI18n(): Promise<void> {
|
||||
store.locale = saved;
|
||||
}
|
||||
await loadDict(store.locale);
|
||||
if (store.locale !== 'en') await loadDict('en');
|
||||
applyHtmlLang(store.locale);
|
||||
store.loaded = true;
|
||||
// Warm the English fallback in the background. `t()` only consults it for
|
||||
// keys the active (complete) locale is missing — and most call sites already
|
||||
// pass an inline English fallback — so it must not block first paint. When it
|
||||
// arrives, `dicts.en` is reactive, so any key that fell through re-renders.
|
||||
if (store.locale !== 'en') void loadDict('en');
|
||||
}
|
||||
|
||||
export async function setLocale(locale: Locale): Promise<boolean> {
|
||||
|
||||
Reference in New Issue
Block a user