mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Add Deutsch across all card surfaces and backend flows, backed by the language registry introduced in #62. German loads as a fingerprint-checked page-shared locale chunk so EN/RU remain synchronous and the initial View budget stays intact. Root render gates prevent mixed-language flashes, retry once, and fail open to English. Extend parity, runtime, bundle, browser and visual coverage, plus contributor and user documentation. Issue: #348 User-Visible: yes
132 lines
4.6 KiB
JavaScript
132 lines
4.6 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import {
|
|
LanguageRuntime, languageRenderGate,
|
|
} from '../test-build/i18n/language-runtime.js';
|
|
|
|
const deferred = () => {
|
|
let resolve;
|
|
let reject;
|
|
const promise = new Promise((ok, fail) => { resolve = ok; reject = fail; });
|
|
return { promise, resolve, reject };
|
|
};
|
|
|
|
test('lazy locale runtime deduplicates hosts and accepts the matching build', async () => {
|
|
const load = deferred();
|
|
let calls = 0;
|
|
const runtime = new LanguageRuntime([
|
|
{ code: 'en', dictionary: { save: 'Save' } },
|
|
{ code: 'de', loadDictionary: async () => { calls++; return load.promise; } },
|
|
], 'same-build');
|
|
|
|
const first = runtime.ensure('de');
|
|
const second = runtime.ensure('de');
|
|
assert.equal(first, second);
|
|
assert.equal(calls, 1);
|
|
assert.equal(runtime.state('de'), 'pending');
|
|
load.resolve({ dictionary: { save: 'Speichern' }, fingerprint: 'same-build' });
|
|
await first;
|
|
assert.equal(runtime.state('de'), 'ready');
|
|
assert.equal(runtime.dictionary('de').save, 'Speichern');
|
|
await runtime.ensure('de');
|
|
assert.equal(calls, 1);
|
|
});
|
|
|
|
test('lazy locale runtime retries once, warns once and settles on English fallback', async () => {
|
|
const attempts = [];
|
|
const warnings = [];
|
|
const runtime = new LanguageRuntime([
|
|
{
|
|
code: 'de',
|
|
loadDictionary: async (attempt) => {
|
|
attempts.push(attempt);
|
|
throw new Error(`failure ${attempt}`);
|
|
},
|
|
},
|
|
], 'build', (message, error) => warnings.push([message, error]));
|
|
|
|
await runtime.ensure('de');
|
|
assert.deepEqual(attempts, [0, 1]);
|
|
assert.equal(runtime.state('de'), 'fallback');
|
|
assert.equal(runtime.dictionary('de'), undefined);
|
|
assert.equal(warnings.length, 1);
|
|
await runtime.ensure('de');
|
|
assert.equal(warnings.length, 1);
|
|
assert.deepEqual(attempts, [0, 1]);
|
|
});
|
|
|
|
test('lazy locale runtime rejects a stale fingerprint before accepting retry', async () => {
|
|
const runtime = new LanguageRuntime([{
|
|
code: 'de',
|
|
loadDictionary: async (attempt) => ({
|
|
dictionary: { save: attempt ? 'Speichern' : 'Stale' },
|
|
fingerprint: attempt ? 'current' : 'old',
|
|
}),
|
|
}], 'current');
|
|
await runtime.ensure('de');
|
|
assert.equal(runtime.dictionary('de').save, 'Speichern');
|
|
});
|
|
|
|
class FakeHost {
|
|
controllers = [];
|
|
inert = false;
|
|
isConnected = true;
|
|
attrs = new Map();
|
|
updates = 0;
|
|
addController(controller) { this.controllers.push(controller); }
|
|
requestUpdate() { this.updates++; }
|
|
hasAttribute(name) { return this.attrs.has(name); }
|
|
setAttribute(name, value) { this.attrs.set(name, value); }
|
|
removeAttribute(name) { this.attrs.delete(name); }
|
|
}
|
|
|
|
test('locale render gate exposes cold/warm states, inertness and ignores disconnected hosts', async () => {
|
|
const load = deferred();
|
|
const runtime = new LanguageRuntime([
|
|
{ code: 'en', dictionary: {} },
|
|
{ code: 'de', loadDictionary: async () => load.promise },
|
|
], 'build');
|
|
const host = new FakeHost();
|
|
let language = 'en';
|
|
assert.equal(languageRenderGate(host, runtime, language), 'ready');
|
|
assert.equal(host.attrs.get('lang'), 'en');
|
|
language = 'de';
|
|
assert.equal(languageRenderGate(host, runtime, language), 'warm');
|
|
assert.equal(host.inert, true);
|
|
assert.equal(host.attrs.get('aria-busy'), 'true');
|
|
host.isConnected = false;
|
|
load.resolve({ dictionary: {}, fingerprint: 'build' });
|
|
await runtime.ensure('de');
|
|
await Promise.resolve();
|
|
assert.equal(host.updates, 0);
|
|
assert.equal(languageRenderGate(host, runtime, language), 'ready');
|
|
assert.equal(host.inert, false);
|
|
assert.equal(host.hasAttribute('aria-busy'), false);
|
|
assert.equal(host.attrs.get('lang'), 'de');
|
|
|
|
const coldLoad = deferred();
|
|
const coldRuntime = new LanguageRuntime([{
|
|
code: 'de', loadDictionary: async () => coldLoad.promise,
|
|
}], 'build');
|
|
const coldHost = new FakeHost();
|
|
assert.equal(languageRenderGate(coldHost, coldRuntime, 'de'), 'cold');
|
|
coldLoad.resolve({ dictionary: {}, fingerprint: 'build' });
|
|
await coldRuntime.ensure('de');
|
|
await Promise.resolve();
|
|
assert.equal(coldHost.updates, 1);
|
|
assert.equal(languageRenderGate(coldHost, coldRuntime, 'de'), 'ready');
|
|
assert.equal(coldHost.attrs.get('lang'), 'de');
|
|
});
|
|
|
|
test('locale render gate exposes the fallback language to assistive technology', async () => {
|
|
const runtime = new LanguageRuntime([{
|
|
code: 'de', loadDictionary: async () => { throw new Error('offline'); },
|
|
}], 'build', () => {});
|
|
const host = new FakeHost();
|
|
assert.equal(languageRenderGate(host, runtime, 'de'), 'cold');
|
|
await runtime.ensure('de');
|
|
assert.equal(languageRenderGate(host, runtime, 'de'), 'ready');
|
|
assert.equal(host.attrs.get('lang'), 'en');
|
|
});
|