feat(core): add i18n foundation with English locale and UI wiring (#1589)

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Wesley Luyten <me@wesleyluyten.com>
This commit is contained in:
Sam Potts
2026-06-18 11:55:38 -07:00
committed by GitHub
co-authored by Cursor Wesley Luyten
parent fa768da757
commit 768bf09da0
79 changed files with 2237 additions and 455 deletions
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest';
import { effectiveLocale } from '../effective-locale';
describe('effectiveLocale', () => {
it('prefers explicit locale over ambient', () => {
expect(effectiveLocale('fr', 'de')).toBe('fr');
});
it('uses ambient when explicit is undefined', () => {
expect(effectiveLocale(undefined, 'es-MX')).toBe('es-MX');
});
it('falls back to en when both missing or blank', () => {
expect(effectiveLocale(undefined, undefined)).toBe('en');
expect(effectiveLocale(' ', undefined)).toBe('en');
expect(effectiveLocale(undefined, ' ')).toBe('en');
});
it('respects custom fallback', () => {
expect(effectiveLocale(undefined, undefined, 'xx')).toBe('xx');
});
});
@@ -0,0 +1,16 @@
import { describe, expect, it } from 'vitest';
import { localeFromDomLang } from '../locale-from-dom-lang';
describe('localeFromDomLang', () => {
it('returns undefined for missing or blank', () => {
expect(localeFromDomLang(undefined)).toBeUndefined();
expect(localeFromDomLang('')).toBeUndefined();
expect(localeFromDomLang(' ')).toBeUndefined();
});
it('returns trimmed language tag', () => {
expect(localeFromDomLang(' fr ')).toBe('fr');
expect(localeFromDomLang('de-DE')).toBe('de-DE');
});
});
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest';
import { mergeLocaleOverlays } from '../merge-locale-overlays';
describe('mergeLocaleOverlays', () => {
it('merges layers least-specific to most-specific', async () => {
const chain = (_locale: string) => ['es', 'en'];
const load = async (tag: string): Promise<Partial<Record<'a' | 'b' | 'c', string>> | undefined> =>
tag === 'en' ? { a: 'en-a', b: 'en-b' } : tag === 'es' ? { b: 'es-b', c: 'es-c' } : undefined;
const { merged, loadedTags } = await mergeLocaleOverlays('es', load, chain);
expect(merged).toEqual({ a: 'en-a', b: 'es-b', c: 'es-c' });
expect(loadedTags).toEqual(['es', 'en']);
});
it('skips undefined layers', async () => {
const chain = () => ['xx', 'en'];
const load = async (tag: string) => (tag === 'en' ? { k: 'v' } : undefined);
const { merged, loadedTags } = await mergeLocaleOverlays('xx', load, chain);
expect(merged).toEqual({ k: 'v' });
expect(loadedTags).toEqual(['en']);
});
});
@@ -0,0 +1,57 @@
import { afterEach, describe, expect, it } from 'vitest';
import { nearestLang } from '../nearest-lang';
describe('nearestLang', () => {
afterEach(() => {
document.body.innerHTML = '';
document.documentElement.removeAttribute('lang');
});
it('returns undefined for null start', () => {
expect(nearestLang(null)).toBeUndefined();
});
it('reads lang on the start element', () => {
const el = document.createElement('div');
el.setAttribute('lang', 'fr');
document.body.appendChild(el);
expect(nearestLang(el)).toBe('fr');
});
it('walks ancestors and prefers closest lang', () => {
const outer = document.createElement('section');
outer.setAttribute('lang', 'de');
const inner = document.createElement('div');
inner.setAttribute('lang', 'fr');
outer.appendChild(inner);
document.body.appendChild(outer);
expect(nearestLang(inner)).toBe('fr');
});
it('inherits from an ancestor when start has no lang', () => {
const outer = document.createElement('section');
outer.setAttribute('lang', 'de');
const inner = document.createElement('div');
outer.appendChild(inner);
document.body.appendChild(outer);
expect(nearestLang(inner)).toBe('de');
});
it('ignores empty lang and continues walking', () => {
const outer = document.createElement('section');
outer.setAttribute('lang', 'de');
const inner = document.createElement('div');
inner.setAttribute('lang', ' ');
outer.appendChild(inner);
document.body.appendChild(outer);
expect(nearestLang(inner)).toBe('de');
});
it('reads lang IDL property on html when set via documentElement.lang', () => {
document.documentElement.lang = 'fr';
const inner = document.createElement('div');
document.body.appendChild(inner);
expect(nearestLang(inner)).toBe('fr');
});
});
@@ -0,0 +1,41 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { subscribeAmbientLang } from '../subscribe-ambient-lang';
describe('subscribeAmbientLang', () => {
afterEach(() => {
document.documentElement.removeAttribute('lang');
vi.restoreAllMocks();
});
it('invokes callback when html lang changes', async () => {
const spy = vi.fn();
const off = subscribeAmbientLang(spy);
document.documentElement.setAttribute('lang', 'de');
await Promise.resolve();
await Promise.resolve();
expect(spy).toHaveBeenCalled();
off();
});
it('invokes callback when html lang property changes', async () => {
const spy = vi.fn();
const off = subscribeAmbientLang(spy);
document.documentElement.lang = 'fr';
await Promise.resolve();
await Promise.resolve();
expect(spy).toHaveBeenCalled();
off();
});
it('unsubscribe stops notifications', async () => {
const spy = vi.fn();
const off = subscribeAmbientLang(spy);
off();
spy.mockClear();
document.documentElement.setAttribute('lang', 'fr');
await Promise.resolve();
await Promise.resolve();
expect(spy).not.toHaveBeenCalled();
});
});