mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
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:
co-authored by
Cursor
Wesley Luyten
parent
fa768da757
commit
768bf09da0
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user