Files
houseplan-card/test/i18n-runtime.test.mjs
T
Matysh 12cd8a1af3 feat: add complete German localization
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
2026-08-28 11:58:46 +03:00

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');
});