Surfaces `App.prefs` and routes lang/format/frontend through the prefs engine when it's declared in the schema. Apps that don't opt into prefs keep the existing lang-driven locale chain untouched. - `defineActivePrefs(options)` — service factory with `initMode: 'immediate'` (consumer factories ask for `prefs` synchronously at construction time; deferring would make the dependency graph order-sensitive). No core dependencies — the engine is pure data. - `defineActiveLang` — when `prefs` is in the schema, subscribes to `prefs.language` and drives `lang.setLocale()` for both the initial value and changes; the subscription is detached on `dispose()`. - `defineActiveFormat` — locale-source resolution is now `options.localeSource` → `prefs.locale` → `lang.locale` → Format default. `prefs.locale` (regional formatting) wins over `prefs.language` (i18n) when both are present. - `defineActiveFrontend` — same precedence chain but uses `prefs.language` (NOT `prefs.locale`) because Frontend's `dir = auto` follows the writing system, which is a property of the language. - `$prefs` alias added to `svelte.config.js`. Sium needs no factory change — it consumes `lang` for translation strings, so the prefs-driven language flows through transitively. Tests: 1 service-factory integration + 4 consumer-wiring integration scenarios covering lang.t() flip, format.getLocale flip, frontend.getLocale flip, and the prefs-less fallback. Full suite: 1674 / 1674 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>master
parent
620a471850
commit
1f8015f2fe
@ -0,0 +1,42 @@
|
|||||||
|
import { createActivePrefs } from '$prefs';
|
||||||
|
import type { ActivePrefs, EnginePrefsOptions } from '$prefs';
|
||||||
|
import type { AppServiceFactory } from '../services.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for `defineActivePrefs`. A thin alias of `EnginePrefsOptions`
|
||||||
|
* — exported here so callers can `import { type DefineActivePrefsOptions }
|
||||||
|
* from '$active-app'` without reaching into `$prefs`.
|
||||||
|
*/
|
||||||
|
export type DefineActivePrefsOptions = EnginePrefsOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `defineActivePrefs(options)` produces a service factory for the
|
||||||
|
* `prefs` slot. Adds the runtime to the App schema as
|
||||||
|
* `App.prefs: ActivePrefs`, where downstream service factories
|
||||||
|
* (`defineActiveLang`, `defineActiveFormat`, `defineActiveFrontend`)
|
||||||
|
* read capability sources via the helpers in `$prefs/sources`.
|
||||||
|
*
|
||||||
|
* No core dependencies: the engine is pure data — capabilities,
|
||||||
|
* environment and intent are passed in by the caller. Environment
|
||||||
|
* detection lives in `$prefs/adapters/*` and is invoked by the
|
||||||
|
* application's bootstrap code, not by this factory.
|
||||||
|
*
|
||||||
|
* `initMode: 'immediate'` because consumer factories ask for `prefs`
|
||||||
|
* via `serviceDependencies` at construction time; deferring the build
|
||||||
|
* would make the dependency graph order-sensitive.
|
||||||
|
*/
|
||||||
|
export function defineActivePrefs(
|
||||||
|
options: DefineActivePrefsOptions
|
||||||
|
): AppServiceFactory<'prefs', readonly [], readonly [], ActivePrefs> {
|
||||||
|
return {
|
||||||
|
name: 'prefs',
|
||||||
|
coreDependencies: [],
|
||||||
|
initMode: 'immediate',
|
||||||
|
create(): ActivePrefs {
|
||||||
|
return createActivePrefs(options);
|
||||||
|
},
|
||||||
|
dispose(instance) {
|
||||||
|
instance.dispose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* Verifies the cross-cutting wiring done by the consumer factories
|
||||||
|
* (`defineActiveLang`, `defineActiveFormat`, `defineActiveFrontend`):
|
||||||
|
* when `prefs` is declared in the schema, those consumers must source
|
||||||
|
* their locale/language from the prefs engine instead of from each
|
||||||
|
* other or from their own defaults.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import type { PrefsCapabilities } from '$libs/prefs';
|
||||||
|
import { createSvelteEngineBus } from '$bus';
|
||||||
|
import { createEngineLogger } from '$logger/engine-logger';
|
||||||
|
import { createEngineOrca } from '$orca';
|
||||||
|
import { createActiveTimers } from '$timer/active-timers.svelte';
|
||||||
|
import { buildServiceBuilders } from '../service-builder.ts';
|
||||||
|
import {
|
||||||
|
defineActiveDom,
|
||||||
|
defineActiveFormat,
|
||||||
|
defineActiveFrontend,
|
||||||
|
defineActiveLang,
|
||||||
|
defineActivePrefs
|
||||||
|
} from '../service-factories/index.ts';
|
||||||
|
import type { CoreServices } from '../services.ts';
|
||||||
|
|
||||||
|
function buildCore(): CoreServices {
|
||||||
|
const logger = createEngineLogger({});
|
||||||
|
const timers = createActiveTimers({ logger });
|
||||||
|
const bus = createSvelteEngineBus({ logger, clock: timers.clock });
|
||||||
|
const orca = createEngineOrca({ bus, timers, logger });
|
||||||
|
return { logger, bus, timers, orca };
|
||||||
|
}
|
||||||
|
|
||||||
|
const CAPS: PrefsCapabilities = {
|
||||||
|
languages: ['es-ES', 'en-US'],
|
||||||
|
locales: ['es-ES', 'en-US'],
|
||||||
|
currencies: ['EUR', 'USD'],
|
||||||
|
unitSystems: ['metric', 'imperial'],
|
||||||
|
themes: ['light', 'dark', 'system'],
|
||||||
|
densities: ['compact', 'comfortable', 'spacious'],
|
||||||
|
motions: ['allow', 'reduce', 'system'],
|
||||||
|
defaults: {
|
||||||
|
language: 'es-ES',
|
||||||
|
locale: 'es-ES',
|
||||||
|
currency: 'EUR',
|
||||||
|
timezone: 'Europe/Madrid',
|
||||||
|
unitSystem: 'metric',
|
||||||
|
theme: 'light',
|
||||||
|
density: 'comfortable',
|
||||||
|
motion: 'allow',
|
||||||
|
direction: 'ltr'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const LANG_SCHEMA = {
|
||||||
|
hello: { 'es-ES': 'Hola', 'en-US': 'Hello' }
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('prefs → consumer wiring', () => {
|
||||||
|
it('lang.setLocale fires when prefs.language changes', () => {
|
||||||
|
const core = buildCore();
|
||||||
|
const builders = buildServiceBuilders(
|
||||||
|
{
|
||||||
|
prefs: defineActivePrefs({ capabilities: CAPS }),
|
||||||
|
lang: defineActiveLang({ schema: LANG_SCHEMA, defaultLocale: 'es-ES' })
|
||||||
|
},
|
||||||
|
core
|
||||||
|
);
|
||||||
|
const { prefs, lang } = builders.proxies as {
|
||||||
|
prefs: { setIntent: (k: 'language', v: string) => unknown };
|
||||||
|
lang: { getLocale: () => string; t: (k: 'hello') => string };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial language flows from prefs (defaults.language = 'es-ES').
|
||||||
|
expect(lang.getLocale()).toBe('es-ES');
|
||||||
|
expect(lang.t('hello')).toBe('Hola');
|
||||||
|
|
||||||
|
// Switching prefs.language triggers lang.setLocale via the
|
||||||
|
// factory's subscription.
|
||||||
|
prefs.setIntent('language', 'en-US');
|
||||||
|
expect(lang.getLocale()).toBe('en-US');
|
||||||
|
expect(lang.t('hello')).toBe('Hello');
|
||||||
|
|
||||||
|
builders.disposeAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('format follows prefs.locale instead of lang when both are declared', () => {
|
||||||
|
const core = buildCore();
|
||||||
|
const builders = buildServiceBuilders(
|
||||||
|
{
|
||||||
|
prefs: defineActivePrefs({ capabilities: CAPS }),
|
||||||
|
lang: defineActiveLang({ schema: LANG_SCHEMA, defaultLocale: 'es-ES' }),
|
||||||
|
format: defineActiveFormat()
|
||||||
|
},
|
||||||
|
core
|
||||||
|
);
|
||||||
|
const { prefs, format } = builders.proxies as {
|
||||||
|
prefs: { setIntent: (k: 'locale', v: string) => unknown };
|
||||||
|
format: { getLocale: () => string };
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(format.getLocale()).toBe('es-ES');
|
||||||
|
|
||||||
|
prefs.setIntent('locale', 'en-US');
|
||||||
|
expect(format.getLocale()).toBe('en-US');
|
||||||
|
|
||||||
|
builders.disposeAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('frontend follows prefs.language for direction derivation', () => {
|
||||||
|
const core = buildCore();
|
||||||
|
const builders = buildServiceBuilders(
|
||||||
|
{
|
||||||
|
prefs: defineActivePrefs({ capabilities: CAPS }),
|
||||||
|
dom: defineActiveDom(),
|
||||||
|
lang: defineActiveLang({ schema: LANG_SCHEMA, defaultLocale: 'es-ES' }),
|
||||||
|
frontend: defineActiveFrontend({ applyDom: false })
|
||||||
|
},
|
||||||
|
core
|
||||||
|
);
|
||||||
|
const { prefs, frontend } = builders.proxies as {
|
||||||
|
prefs: { setIntent: (k: 'language', v: string) => unknown };
|
||||||
|
frontend: { getLocale: () => string };
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(frontend.getLocale()).toBe('es-ES');
|
||||||
|
|
||||||
|
prefs.setIntent('language', 'en-US');
|
||||||
|
expect(frontend.getLocale()).toBe('en-US');
|
||||||
|
|
||||||
|
builders.disposeAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('format / frontend retain the lang fallback when prefs is not declared', () => {
|
||||||
|
const core = buildCore();
|
||||||
|
const builders = buildServiceBuilders(
|
||||||
|
{
|
||||||
|
lang: defineActiveLang({ schema: LANG_SCHEMA, defaultLocale: 'en-US' }),
|
||||||
|
format: defineActiveFormat()
|
||||||
|
},
|
||||||
|
core
|
||||||
|
);
|
||||||
|
const { format } = builders.proxies as {
|
||||||
|
format: { getLocale: () => string };
|
||||||
|
};
|
||||||
|
// No prefs in schema → format falls back to lang's locale.
|
||||||
|
expect(format.getLocale()).toBe('en-US');
|
||||||
|
builders.disposeAll();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in new issue